Files
sonr/packages/ui/src/components/blocks/sign-in-flow.tsx
T
Prad NukalaandGitHub 13e6c3e84d Master (#1262)
* clear

* feat: Add everything

* fix: Commenht
2025-10-03 14:45:52 -04:00

897 lines
33 KiB
TypeScript

'use client';
import { cn } from '@/lib/utils';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { AnimatePresence, motion } from 'framer-motion';
import Link from 'next/link';
import React, { useState, useMemo, useRef, useEffect } from 'react';
import * as THREE from 'three';
type Uniforms = {
[key: string]: {
value: number[] | number[][] | number;
type: string;
};
};
interface ShaderProps {
source: string;
uniforms: {
[key: string]: {
value: number[] | number[][] | number;
type: string;
};
};
maxFps?: number;
}
interface SignInPageProps {
className?: string;
}
export const CanvasRevealEffect = ({
animationSpeed = 10,
opacities = [0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.8, 0.8, 0.8, 1],
colors = [[0, 255, 255]],
containerClassName,
dotSize,
showGradient = true,
reverse = false, // This controls the direction
}: {
animationSpeed?: number;
opacities?: number[];
colors?: number[][];
containerClassName?: string;
dotSize?: number;
showGradient?: boolean;
reverse?: boolean; // This prop determines the direction
}) => {
return (
<div className={cn('h-full relative w-full', containerClassName)}>
{' '}
{/* Removed bg-white */}
<div className="h-full w-full">
<DotMatrix
colors={colors ?? [[0, 255, 255]]}
dotSize={dotSize ?? 3}
opacities={opacities ?? [0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.8, 0.8, 0.8, 1]}
// Pass reverse state and speed via string flags in the empty shader prop
shader={`
${reverse ? 'u_reverse_active' : 'false'}_;
animation_speed_factor_${animationSpeed.toFixed(1)}_;
`}
center={['x', 'y']}
/>
</div>
{showGradient && (
// Adjust gradient colors if needed based on background (was bg-white, now likely uses containerClassName bg)
// Example assuming a dark background like the SignInPage uses:
<div className="absolute inset-0 bg-gradient-to-t from-black to-transparent" />
)}
</div>
);
};
interface DotMatrixProps {
colors?: number[][];
opacities?: number[];
totalSize?: number;
dotSize?: number;
shader?: string;
center?: ('x' | 'y')[];
}
const DotMatrix: React.FC<DotMatrixProps> = ({
colors = [[0, 0, 0]],
opacities = [0.04, 0.04, 0.04, 0.04, 0.04, 0.08, 0.08, 0.08, 0.08, 0.14],
totalSize = 20,
dotSize = 2,
shader = '', // This shader string will now contain the animation logic
center = ['x', 'y'],
}) => {
// ... uniforms calculation remains the same for colors, opacities, etc.
const uniforms = React.useMemo(() => {
let colorsArray = [colors[0], colors[0], colors[0], colors[0], colors[0], colors[0]];
if (colors.length === 2) {
colorsArray = [colors[0], colors[0], colors[0], colors[1], colors[1], colors[1]];
} else if (colors.length === 3) {
colorsArray = [colors[0], colors[0], colors[1], colors[1], colors[2], colors[2]];
}
return {
u_colors: {
value: colorsArray.map((color) => [
(color?.[0] ?? 0) / 255,
(color?.[1] ?? 0) / 255,
(color?.[2] ?? 0) / 255,
]),
type: 'uniform3fv',
},
u_opacities: {
value: opacities,
type: 'uniform1fv',
},
u_total_size: {
value: totalSize,
type: 'uniform1f',
},
u_dot_size: {
value: dotSize,
type: 'uniform1f',
},
u_reverse: {
value: shader.includes('u_reverse_active') ? 1 : 0, // Convert boolean to number (1 or 0)
type: 'uniform1i', // Use 1i for bool in WebGL1/GLSL100, or just bool for GLSL300+ if supported
},
};
}, [colors, opacities, totalSize, dotSize, shader]); // Add shader to dependencies
return (
<Shader
// The main animation logic is now built *outside* the shader prop
source={`
precision mediump float;
in vec2 fragCoord;
uniform float u_time;
uniform float u_opacities[10];
uniform vec3 u_colors[6];
uniform float u_total_size;
uniform float u_dot_size;
uniform vec2 u_resolution;
uniform int u_reverse; // Changed from bool to int
out vec4 fragColor;
float PHI = 1.61803398874989484820459;
float random(vec2 xy) {
return fract(tan(distance(xy * PHI, xy) * 0.5) * xy.x);
}
float map(float value, float min1, float max1, float min2, float max2) {
return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
}
void main() {
vec2 st = fragCoord.xy;
${
center.includes('x')
? 'st.x -= abs(floor((mod(u_resolution.x, u_total_size) - u_dot_size) * 0.5));'
: ''
}
${
center.includes('y')
? 'st.y -= abs(floor((mod(u_resolution.y, u_total_size) - u_dot_size) * 0.5));'
: ''
}
float opacity = step(0.0, st.x);
opacity *= step(0.0, st.y);
vec2 st2 = vec2(int(st.x / u_total_size), int(st.y / u_total_size));
float frequency = 5.0;
float show_offset = random(st2); // Used for initial opacity random pick and color
float rand = random(st2 * floor((u_time / frequency) + show_offset + frequency));
opacity *= u_opacities[int(rand * 10.0)];
opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.x / u_total_size));
opacity *= 1.0 - step(u_dot_size / u_total_size, fract(st.y / u_total_size));
vec3 color = u_colors[int(show_offset * 6.0)];
// --- Animation Timing Logic ---
float animation_speed_factor = 0.5; // Extract speed from shader string
vec2 center_grid = u_resolution / 2.0 / u_total_size;
float dist_from_center = distance(center_grid, st2);
// Calculate timing offset for Intro (from center)
float timing_offset_intro = dist_from_center * 0.01 + (random(st2) * 0.15);
// Calculate timing offset for Outro (from edges)
// Max distance from center to a corner of the grid
float max_grid_dist = distance(center_grid, vec2(0.0, 0.0));
float timing_offset_outro = (max_grid_dist - dist_from_center) * 0.02 + (random(st2 + 42.0) * 0.2);
float current_timing_offset;
if (u_reverse == 1) {
current_timing_offset = timing_offset_outro;
// Outro logic: opacity starts high, goes to 0 when time passes offset
opacity *= 1.0 - step(current_timing_offset, u_time * animation_speed_factor);
// Clamp for fade-out transition
opacity *= clamp((step(current_timing_offset + 0.1, u_time * animation_speed_factor)) * 1.25, 1.0, 1.25);
} else {
current_timing_offset = timing_offset_intro;
// Intro logic: opacity starts 0, goes to base opacity when time passes offset
opacity *= step(current_timing_offset, u_time * animation_speed_factor);
// Clamp for fade-in transition
opacity *= clamp((1.0 - step(current_timing_offset + 0.1, u_time * animation_speed_factor)) * 1.25, 1.0, 1.25);
}
fragColor = vec4(color, opacity);
fragColor.rgb *= fragColor.a; // Premultiply alpha
}`}
uniforms={uniforms}
maxFps={60}
/>
);
};
const ShaderMaterial = ({
source,
uniforms,
}: {
source: string;
hovered?: boolean;
maxFps?: number;
uniforms: Uniforms;
}) => {
const { size } = useThree();
const ref = useRef<THREE.Mesh>(null);
useFrame(({ clock }) => {
if (!ref.current) return;
const timestamp = clock.getElapsedTime();
const material: any = ref.current.material;
const timeLocation = material.uniforms.u_time;
timeLocation.value = timestamp;
});
const getUniforms = () => {
const preparedUniforms: any = {};
for (const uniformName in uniforms) {
const uniform: any = uniforms[uniformName];
switch (uniform.type) {
case 'uniform1f':
preparedUniforms[uniformName] = { value: uniform.value, type: '1f' };
break;
case 'uniform1i':
preparedUniforms[uniformName] = { value: uniform.value, type: '1i' };
break;
case 'uniform3f':
preparedUniforms[uniformName] = {
value: new THREE.Vector3().fromArray(uniform.value),
type: '3f',
};
break;
case 'uniform1fv':
preparedUniforms[uniformName] = { value: uniform.value, type: '1fv' };
break;
case 'uniform3fv':
preparedUniforms[uniformName] = {
value: uniform.value.map((v: number[]) => new THREE.Vector3().fromArray(v)),
type: '3fv',
};
break;
case 'uniform2f':
preparedUniforms[uniformName] = {
value: new THREE.Vector2().fromArray(uniform.value),
type: '2f',
};
break;
default:
console.error(`Invalid uniform type for '${uniformName}'.`);
break;
}
}
preparedUniforms.u_time = { value: 0, type: '1f' };
preparedUniforms.u_resolution = {
value: new THREE.Vector2(size.width * 2, size.height * 2),
}; // Initialize u_resolution
return preparedUniforms;
};
// Shader material
const material = useMemo(() => {
const materialObject = new THREE.ShaderMaterial({
vertexShader: `
precision mediump float;
in vec2 coordinates;
uniform vec2 u_resolution;
out vec2 fragCoord;
void main(){
float x = position.x;
float y = position.y;
gl_Position = vec4(x, y, 0.0, 1.0);
fragCoord = (position.xy + vec2(1.0)) * 0.5 * u_resolution;
fragCoord.y = u_resolution.y - fragCoord.y;
}
`,
fragmentShader: source,
uniforms: getUniforms(),
glslVersion: THREE.GLSL3,
blending: THREE.CustomBlending,
blendSrc: THREE.SrcAlphaFactor,
blendDst: THREE.OneFactor,
});
return materialObject;
}, [size.width, size.height, source]);
return (
<mesh ref={ref as any}>
<planeGeometry args={[2, 2]} />
<primitive object={material} attach="material" />
</mesh>
);
};
const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => {
return (
<Canvas className="absolute inset-0 h-full w-full">
<ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} />
</Canvas>
);
};
const AnimatedNavLink = ({ href, children }: { href: string; children: React.ReactNode }) => {
const defaultTextColor = 'text-gray-300';
const hoverTextColor = 'text-white';
const textSizeClass = 'text-sm';
return (
<a
href={href}
className={`group relative inline-block overflow-hidden h-5 flex items-center ${textSizeClass}`}
>
<div className="flex flex-col transition-transform duration-400 ease-out transform group-hover:-translate-y-1/2">
<span className={defaultTextColor}>{children}</span>
<span className={hoverTextColor}>{children}</span>
</div>
</a>
);
};
function MiniNavbar() {
const [isOpen, setIsOpen] = useState(false);
const [headerShapeClass, setHeaderShapeClass] = useState('rounded-full');
const shapeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const toggleMenu = () => {
setIsOpen(!isOpen);
};
useEffect(() => {
if (shapeTimeoutRef.current) {
clearTimeout(shapeTimeoutRef.current);
}
if (isOpen) {
setHeaderShapeClass('rounded-xl');
} else {
shapeTimeoutRef.current = setTimeout(() => {
setHeaderShapeClass('rounded-full');
}, 300);
}
return () => {
if (shapeTimeoutRef.current) {
clearTimeout(shapeTimeoutRef.current);
}
};
}, [isOpen]);
const logoElement = (
<div className="relative w-5 h-5 flex items-center justify-center">
<span className="absolute w-1.5 h-1.5 rounded-full bg-gray-200 top-0 left-1/2 transform -translate-x-1/2 opacity-80" />
<span className="absolute w-1.5 h-1.5 rounded-full bg-gray-200 left-0 top-1/2 transform -translate-y-1/2 opacity-80" />
<span className="absolute w-1.5 h-1.5 rounded-full bg-gray-200 right-0 top-1/2 transform -translate-y-1/2 opacity-80" />
<span className="absolute w-1.5 h-1.5 rounded-full bg-gray-200 bottom-0 left-1/2 transform -translate-x-1/2 opacity-80" />
</div>
);
const navLinksData = [
{ label: 'Manifesto', href: '#1' },
{ label: 'Careers', href: '#2' },
{ label: 'Discover', href: '#3' },
];
const loginButtonElement = (
<button className="px-4 py-2 sm:px-3 text-xs sm:text-sm border border-[#333] bg-[rgba(31,31,31,0.62)] text-gray-300 rounded-full hover:border-white/50 hover:text-white transition-colors duration-200 w-full sm:w-auto">
LogIn
</button>
);
const signupButtonElement = (
<div className="relative group w-full sm:w-auto">
<div
className="absolute inset-0 -m-2 rounded-full
hidden sm:block
bg-gray-100
opacity-40 filter blur-lg pointer-events-none
transition-all duration-300 ease-out
group-hover:opacity-60 group-hover:blur-xl group-hover:-m-3"
/>
<button className="relative z-10 px-4 py-2 sm:px-3 text-xs sm:text-sm font-semibold text-black bg-gradient-to-br from-gray-100 to-gray-300 rounded-full hover:from-gray-200 hover:to-gray-400 transition-all duration-200 w-full sm:w-auto">
Signup
</button>
</div>
);
return (
<header
className={`fixed top-6 left-1/2 transform -translate-x-1/2 z-20
flex flex-col items-center
pl-6 pr-6 py-3 backdrop-blur-sm
${headerShapeClass}
border border-[#333] bg-[#1f1f1f57]
w-[calc(100%-2rem)] sm:w-auto
transition-[border-radius] duration-0 ease-in-out`}
>
<div className="flex items-center justify-between w-full gap-x-6 sm:gap-x-8">
<div className="flex items-center">{logoElement}</div>
<nav className="hidden sm:flex items-center space-x-4 sm:space-x-6 text-sm">
{navLinksData.map((link) => (
<AnimatedNavLink key={link.href} href={link.href}>
{link.label}
</AnimatedNavLink>
))}
</nav>
<div className="hidden sm:flex items-center gap-2 sm:gap-3">
{loginButtonElement}
{signupButtonElement}
</div>
<button
className="sm:hidden flex items-center justify-center w-8 h-8 text-gray-300 focus:outline-none"
onClick={toggleMenu}
aria-label={isOpen ? 'Close Menu' : 'Open Menu'}
>
{isOpen ? (
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
) : (
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
)}
</button>
</div>
<div
className={`sm:hidden flex flex-col items-center w-full transition-all ease-in-out duration-300 overflow-hidden
${isOpen ? 'max-h-[1000px] opacity-100 pt-4' : 'max-h-0 opacity-0 pt-0 pointer-events-none'}`}
>
<nav className="flex flex-col items-center space-y-4 text-base w-full">
{navLinksData.map((link) => (
<a
key={link.href}
href={link.href}
className="text-gray-300 hover:text-white transition-colors w-full text-center"
>
{link.label}
</a>
))}
</nav>
<div className="flex flex-col items-center space-y-4 mt-4 w-full">
{loginButtonElement}
{signupButtonElement}
</div>
</div>
</header>
);
}
export const SignInPage = ({ className }: SignInPageProps) => {
const [email, setEmail] = useState('');
const [step, setStep] = useState<'email' | 'code' | 'success'>('email');
const [code, setCode] = useState(['', '', '', '', '', '']);
const codeInputRefs = useRef<(HTMLInputElement | null)[]>([]);
const [initialCanvasVisible, setInitialCanvasVisible] = useState(true);
const [reverseCanvasVisible, setReverseCanvasVisible] = useState(false);
const handleEmailSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (email) {
setStep('code');
}
};
// Focus first input when code screen appears
useEffect(() => {
if (step === 'code') {
setTimeout(() => {
codeInputRefs.current[0]?.focus();
}, 500);
}
}, [step]);
const handleCodeChange = (index: number, value: string) => {
if (value.length <= 1) {
const newCode = [...code];
newCode[index] = value;
setCode(newCode);
// Focus next input if value is entered
if (value && index < 5) {
codeInputRefs.current[index + 1]?.focus();
}
// Check if code is complete
if (index === 5 && value) {
const isComplete = newCode.every((digit) => digit.length === 1);
if (isComplete) {
// First show the new reverse canvas
setReverseCanvasVisible(true);
// Then hide the original canvas after a small delay
setTimeout(() => {
setInitialCanvasVisible(false);
}, 50);
// Transition to success screen after animation
setTimeout(() => {
setStep('success');
}, 2000);
}
}
}
};
const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Backspace' && !code[index] && index > 0) {
codeInputRefs.current[index - 1]?.focus();
}
};
const handleBackClick = () => {
setStep('email');
setCode(['', '', '', '', '', '']);
// Reset animations if going back
setReverseCanvasVisible(false);
setInitialCanvasVisible(true);
};
return (
<div className={cn('flex w-[100%] flex-col min-h-screen bg-black relative', className)}>
<div className="absolute inset-0 z-0">
{/* Initial canvas (forward animation) */}
{initialCanvasVisible && (
<div className="absolute inset-0">
<CanvasRevealEffect
animationSpeed={3}
containerClassName="bg-black"
colors={[
[255, 255, 255],
[255, 255, 255],
]}
dotSize={6}
reverse={false}
/>
</div>
)}
{/* Reverse canvas (appears when code is complete) */}
{reverseCanvasVisible && (
<div className="absolute inset-0">
<CanvasRevealEffect
animationSpeed={4}
containerClassName="bg-black"
colors={[
[255, 255, 255],
[255, 255, 255],
]}
dotSize={6}
reverse={true}
/>
</div>
)}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_rgba(0,0,0,1)_0%,_transparent_100%)]" />
<div className="absolute top-0 left-0 right-0 h-1/3 bg-gradient-to-b from-black to-transparent" />
</div>
{/* Content Layer */}
<div className="relative z-10 flex flex-col flex-1">
{/* Top navigation */}
<MiniNavbar />
{/* Main content container */}
<div className="flex flex-1 flex-col lg:flex-row ">
{/* Left side (form) */}
<div className="flex-1 flex flex-col justify-center items-center">
<div className="w-full mt-[150px] max-w-sm">
<AnimatePresence mode="wait">
{step === 'email' ? (
<motion.div
key="email-step"
initial={{ opacity: 0, x: -100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100 }}
transition={{ duration: 0.4, ease: 'easeOut' }}
className="space-y-6 text-center"
>
<div className="space-y-1">
<h1 className="text-[2.5rem] font-bold leading-[1.1] tracking-tight text-white">
Welcome Developer
</h1>
<p className="text-[1.8rem] text-white/70 font-light">
Your sign in component
</p>
</div>
<div className="space-y-4">
<button className="backdrop-blur-[2px] w-full flex items-center justify-center gap-2 bg-white/5 hover:bg-white/10 text-white border border-white/10 rounded-full py-3 px-4 transition-colors">
<span className="text-lg">G</span>
<span>Sign in with Google</span>
</button>
<div className="flex items-center gap-4">
<div className="h-px bg-white/10 flex-1" />
<span className="text-white/40 text-sm">or</span>
<div className="h-px bg-white/10 flex-1" />
</div>
<form onSubmit={handleEmailSubmit}>
<div className="relative">
<input
type="email"
placeholder="info@gmail.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full backdrop-blur-[1px] text-white border-1 border-white/10 rounded-full py-3 px-4 focus:outline-none focus:border focus:border-white/30 text-center"
required
/>
<button
type="submit"
className="absolute right-1.5 top-1.5 text-white w-9 h-9 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 transition-colors group overflow-hidden"
>
<span className="relative w-full h-full block overflow-hidden">
<span className="absolute inset-0 flex items-center justify-center transition-transform duration-300 group-hover:translate-x-full">
</span>
<span className="absolute inset-0 flex items-center justify-center transition-transform duration-300 -translate-x-full group-hover:translate-x-0">
</span>
</span>
</button>
</div>
</form>
</div>
<p className="text-xs text-white/40 pt-10">
By signing up, you agree to the{' '}
<Link
href="#"
className="underline text-white/40 hover:text-white/60 transition-colors"
>
MSA
</Link>
,{' '}
<Link
href="#"
className="underline text-white/40 hover:text-white/60 transition-colors"
>
Product Terms
</Link>
,{' '}
<Link
href="#"
className="underline text-white/40 hover:text-white/60 transition-colors"
>
Policies
</Link>
,{' '}
<Link
href="#"
className="underline text-white/40 hover:text-white/60 transition-colors"
>
Privacy Notice
</Link>
, and{' '}
<Link
href="#"
className="underline text-white/40 hover:text-white/60 transition-colors"
>
Cookie Notice
</Link>
.
</p>
</motion.div>
) : step === 'code' ? (
<motion.div
key="code-step"
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 100 }}
transition={{ duration: 0.4, ease: 'easeOut' }}
className="space-y-6 text-center"
>
<div className="space-y-1">
<h1 className="text-[2.5rem] font-bold leading-[1.1] tracking-tight text-white">
We sent you a code
</h1>
<p className="text-[1.25rem] text-white/50 font-light">Please enter it</p>
</div>
<div className="w-full">
<div className="relative rounded-full py-4 px-5 border border-white/10 bg-transparent">
<div className="flex items-center justify-center">
{code.map((digit, i) => (
<div key={i} className="flex items-center">
<div className="relative">
<input
ref={(el) => {
codeInputRefs.current[i] = el;
}}
type="text"
inputMode="numeric"
pattern="[0-9]*"
maxLength={1}
value={digit}
onChange={(e) => handleCodeChange(i, e.target.value)}
onKeyDown={(e) => handleKeyDown(i, e)}
className="w-8 text-center text-xl bg-transparent text-white border-none focus:outline-none focus:ring-0 appearance-none"
style={{ caretColor: 'transparent' }}
/>
{!digit && (
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center pointer-events-none">
<span className="text-xl text-white">0</span>
</div>
)}
</div>
{i < 5 && <span className="text-white/20 text-xl">|</span>}
</div>
))}
</div>
</div>
</div>
<div>
<motion.p
className="text-white/50 hover:text-white/70 transition-colors cursor-pointer text-sm"
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }}
>
Resend code
</motion.p>
</div>
<div className="flex w-full gap-3">
<motion.button
onClick={handleBackClick}
className="rounded-full bg-white text-black font-medium px-8 py-3 hover:bg-white/90 transition-colors w-[30%]"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.2 }}
>
Back
</motion.button>
<motion.button
className={`flex-1 rounded-full font-medium py-3 border transition-all duration-300 ${
code.every((d) => d !== '')
? 'bg-white text-black border-transparent hover:bg-white/90 cursor-pointer'
: 'bg-[#111] text-white/50 border-white/10 cursor-not-allowed'
}`}
disabled={!code.every((d) => d !== '')}
>
Continue
</motion.button>
</div>
<div className="pt-16">
<p className="text-xs text-white/40">
By signing up, you agree to the{' '}
<Link
href="#"
className="underline text-white/40 hover:text-white/60 transition-colors"
>
MSA
</Link>
,{' '}
<Link
href="#"
className="underline text-white/40 hover:text-white/60 transition-colors"
>
Product Terms
</Link>
,{' '}
<Link
href="#"
className="underline text-white/40 hover:text-white/60 transition-colors"
>
Policies
</Link>
,{' '}
<Link
href="#"
className="underline text-white/40 hover:text-white/60 transition-colors"
>
Privacy Notice
</Link>
, and{' '}
<Link
href="#"
className="underline text-white/40 hover:text-white/60 transition-colors"
>
Cookie Notice
</Link>
.
</p>
</div>
</motion.div>
) : (
<motion.div
key="success-step"
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: 'easeOut', delay: 0.3 }}
className="space-y-6 text-center"
>
<div className="space-y-1">
<h1 className="text-[2.5rem] font-bold leading-[1.1] tracking-tight text-white">
You're in!
</h1>
<p className="text-[1.25rem] text-white/50 font-light">Welcome</p>
</div>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.5, delay: 0.5 }}
className="py-10"
>
<div className="mx-auto w-16 h-16 rounded-full bg-gradient-to-br from-white to-white/70 flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-8 w-8 text-black"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</div>
</motion.div>
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
className="w-full rounded-full bg-white text-black font-medium py-3 hover:bg-white/90 transition-colors"
>
Continue to Dashboard
</motion.button>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</div>
</div>
);
};