mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-04 10:21:40 +00:00
897 lines
33 KiB
TypeScript
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>
|
|
);
|
|
};
|