mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-05 02:41:41 +00:00
@@ -0,0 +1,896 @@
|
||||
'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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user