New design v0.2

New design and framework
This commit is contained in:
Oussama Douhou
2026-01-13 10:49:47 +01:00
parent 21161c6554
commit 8977a6fdee
29 changed files with 2434 additions and 58 deletions

View File

@@ -0,0 +1,35 @@
import { TypewriterText } from './TypewriterText';
import type { TranslationKey } from '@/lib/i18n';
interface DeployErrorProps {
t: (key: TranslationKey) => string;
errorMessage: string;
onTryAgain: () => void;
}
export function DeployError({ t, errorMessage, onTryAgain }: DeployErrorProps) {
return (
<div className="backdrop-blur-md bg-black/20 rounded-2xl p-10 border border-white/10 shadow-2xl text-center animate-fadeIn">
<div className="mb-6 animate-shake">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full border-2 border-red-500 text-red-500 text-5xl bg-red-500/10">
</div>
</div>
<h2 className="text-2xl font-bold mb-4 min-h-[2.4rem] text-white">
<TypewriterText text={t('deploymentFailed')} speed={30} />
</h2>
<div className="mb-8 p-4 bg-red-500/10 border border-red-500/20 rounded-xl font-mono text-sm text-red-200">
{errorMessage}
</div>
<button
onClick={onTryAgain}
className="w-full py-3.5 px-6 rounded-xl font-semibold bg-white text-black hover:bg-white/90 transition-colors"
>
{t('tryAgain')}
</button>
</div>
);
}

View File

@@ -0,0 +1,148 @@
import { useState, useEffect, useRef } from 'react';
import { cn } from '@/lib/utils';
import { validateStackName } from '@/lib/validation';
import { TypewriterText } from './TypewriterText';
import type { TranslationKey } from '@/lib/i18n';
interface DeployFormProps {
t: (key: TranslationKey) => string;
onDeploy: (name: string) => void;
isDeploying: boolean;
}
type ValidationStatus = 'idle' | 'checking' | 'valid' | 'invalid';
export function DeployForm({ t, onDeploy, isDeploying }: DeployFormProps) {
const [name, setName] = useState('');
const [validationStatus, setValidationStatus] = useState<ValidationStatus>('idle');
const [validationMessage, setValidationMessage] = useState('');
const checkTimeoutRef = useRef<NodeJS.Timeout>(undefined);
useEffect(() => {
if (!name) {
setValidationStatus('idle');
setValidationMessage('');
return;
}
const validation = validateStackName(name);
if (!validation.valid && validation.error) {
setValidationStatus('invalid');
setValidationMessage(t(validation.error));
return;
}
setValidationStatus('checking');
setValidationMessage(t('checkingAvailability'));
if (checkTimeoutRef.current) {
clearTimeout(checkTimeoutRef.current);
}
checkTimeoutRef.current = setTimeout(async () => {
try {
const response = await fetch(`/api/check/${validation.name}`);
const data = await response.json();
if (data.available && data.valid) {
setValidationStatus('valid');
setValidationMessage(t('nameAvailable'));
} else {
setValidationStatus('invalid');
setValidationMessage(data.error || t('nameNotAvailable'));
}
} catch {
setValidationStatus('invalid');
setValidationMessage(t('checkFailed'));
}
}, 500);
return () => {
if (checkTimeoutRef.current) {
clearTimeout(checkTimeoutRef.current);
}
};
}, [name, t]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validationStatus === 'valid') {
onDeploy(name.trim().toLowerCase());
}
};
const previewName = name || t('yournamePlaceholder');
return (
<div className="backdrop-blur-md bg-black/20 rounded-2xl p-10 border border-white/10 shadow-2xl transition-all hover:border-white/20">
<h2 className="text-2xl font-bold mb-4 min-h-[2.4rem] text-white">
<TypewriterText text={t('chooseStackName')} />
</h2>
<p className="text-gray-400 mb-8 text-[0.95rem]">
{t('availableAt')}{' '}
<strong className="text-blue-400 font-medium">
{previewName}.ai.flexinit.nl
</strong>
</p>
<form onSubmit={handleSubmit}>
<div className="mb-6">
<label className="block font-medium mb-2 text-gray-400 text-sm">
{t('stackName')}
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value.toLowerCase())}
placeholder={t('placeholder')}
className={cn(
'w-full py-3 px-4 bg-white/5 border border-white/10 rounded-xl',
'text-xl font-mono text-white placeholder-gray-600',
'focus:outline-none focus:border-white/30 focus:bg-white/10 transition-all',
validationStatus === 'invalid' && 'border-red-500/50 focus:border-red-500',
validationStatus === 'valid' && 'border-green-500/50 focus:border-green-500',
validationStatus === 'checking' && 'border-blue-400/50 focus:border-blue-400'
)}
autoComplete="off"
disabled={isDeploying}
/>
<p className="text-sm text-gray-500 mt-2">{t('inputHint')}</p>
{validationMessage && (
<p
className={cn(
'text-sm mt-2 min-h-[1.25rem]',
validationStatus === 'invalid' && 'text-red-400',
validationStatus === 'valid' && 'text-green-400',
validationStatus === 'checking' && 'text-gray-400'
)}
>
{validationMessage}
</p>
)}
</div>
<button
type="submit"
disabled={validationStatus !== 'valid' || isDeploying}
className={cn(
'w-full flex items-center justify-center gap-2',
'py-3.5 px-6 rounded-xl font-semibold text-base',
'transition-all duration-300',
validationStatus === 'valid' && !isDeploying
? 'bg-white text-black hover:bg-white/90 shadow-[0_0_15px_rgba(255,255,255,0.1)] hover:translate-y-[-2px]'
: 'bg-white/5 text-gray-500 border border-white/5 cursor-not-allowed'
)}
>
<span>{isDeploying ? t('deployingText') : t('deployBtn')}</span>
{!isDeploying && (
<svg className="w-5 h-5 rtl:scale-x-[-1]" viewBox="0 0 20 20" fill="none">
<path d="M10 4V16M4 10H16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
)}
</button>
</form>
</div>
);
}

View File

@@ -0,0 +1,67 @@
import { cn } from '@/lib/utils';
import type { TranslationKey } from '@/lib/i18n';
interface ProgressData {
progress: number;
message: string;
}
interface DeployProgressProps {
t: (key: TranslationKey) => string;
stackName: string;
deploymentUrl: string;
progressData: ProgressData;
logs: string[];
}
export function DeployProgress({ t, stackName, deploymentUrl, progressData, logs }: DeployProgressProps) {
return (
<div className="backdrop-blur-md bg-black/20 rounded-2xl p-10 border border-white/10 shadow-2xl animate-fadeIn">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-white">{t('deploying')}</h2>
<div className="w-6 h-6 border-3 border-white/10 border-t-white rounded-full animate-spin" />
</div>
<div className="bg-white/5 p-4 rounded-xl mb-6 border border-white/10">
<p className="text-[0.95rem] mb-2 text-gray-300">
{t('stack')}: <strong className="text-white">{stackName}</strong>
</p>
<p className="text-[0.95rem] text-gray-300">
URL: <strong className="text-white">{deploymentUrl}</strong>
</p>
</div>
<div className="flex items-center gap-4 mb-8">
<div className="flex-1 h-1 bg-white/10 rounded overflow-hidden">
<div
className="h-full bg-white transition-all duration-300 rounded"
style={{ width: `${progressData.progress}%` }}
/>
</div>
<span className="font-medium text-gray-400 min-w-[3rem] text-right text-sm">
{progressData.progress}%
</span>
</div>
<div className="flex flex-col gap-4">
<div className={cn(
'flex items-center gap-4 p-3 rounded-lg',
'bg-white/5 border-l-[3px] border-white'
)}>
<span className="text-2xl"></span>
<span className="flex-1 text-[0.95rem] text-gray-300">{progressData.message}</span>
</div>
</div>
{logs.length > 0 && (
<div className="mt-6 p-4 bg-black/50 border border-white/10 rounded-lg font-mono text-sm text-gray-400 max-h-[200px] overflow-y-auto">
{logs.map((log, i) => (
<div key={i} className="animate-fadeInUp">
{log}
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,66 @@
import { TypewriterText } from './TypewriterText';
import type { TranslationKey } from '@/lib/i18n';
interface DeploySuccessProps {
t: (key: TranslationKey) => string;
stackName: string;
deploymentUrl: string;
onDeployAnother: () => void;
}
export function DeploySuccess({ t, stackName, deploymentUrl, onDeployAnother }: DeploySuccessProps) {
return (
<div className="backdrop-blur-md bg-black/20 rounded-2xl p-10 border border-white/10 shadow-2xl text-center animate-fadeIn">
<div className="mb-6 animate-scaleIn">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full border-2 border-green-500 text-green-500 text-5xl bg-green-500/10">
</div>
</div>
<h2 className="text-2xl font-bold mb-4 min-h-[2.4rem] text-white">
<TypewriterText text={t('deploymentComplete')} />
</h2>
<p className="text-gray-400 mb-8">{t('successMessage')}</p>
<div className="bg-white/5 p-6 rounded-xl mb-8 border border-white/10 text-left">
<div className="flex justify-between mb-4 text-[0.95rem]">
<span className="text-gray-400 font-medium">{t('stackNameLabel')}</span>
<span className="font-semibold font-mono text-white">{stackName}</span>
</div>
<div className="flex justify-between text-[0.95rem]">
<span className="text-gray-400 font-medium">URL:</span>
<a
href={deploymentUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 font-semibold font-mono hover:underline"
>
{deploymentUrl}
</a>
</div>
</div>
<div className="space-y-3">
<a
href={deploymentUrl}
target="_blank"
rel="noopener noreferrer"
className="w-full flex items-center justify-center gap-2 py-3.5 px-6 rounded-xl font-semibold bg-white text-black hover:bg-white/90 transition-all animate-pulse-glow"
>
<span>{t('openStack')}</span>
<svg className="w-5 h-5 rtl:scale-x-[-1]" viewBox="0 0 20 20" fill="none">
<path d="M10 4L16 10L10 16M16 10H4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</a>
<button
onClick={onDeployAnother}
className="w-full py-3.5 px-6 rounded-xl font-semibold bg-transparent text-gray-400 border border-white/10 hover:bg-white/5 hover:border-white/20 hover:text-white transition-colors"
>
{t('deployAnother')}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,37 @@
import { cn } from '@/lib/utils';
import type { Language } from '@/lib/i18n';
interface LanguageSelectorProps {
currentLang: Language;
onLangChange: (lang: Language) => void;
}
const languages: { code: Language; label: string; title: string }[] = [
{ code: 'nl', label: 'NL', title: 'Nederlands' },
{ code: 'ar', label: 'AR', title: 'العربية' },
{ code: 'en', label: 'EN', title: 'English' },
];
export function LanguageSelector({ currentLang, onLangChange }: LanguageSelectorProps) {
return (
<div className="fixed top-4 right-4 rtl:right-auto rtl:left-4 flex gap-2 z-50">
{languages.map(({ code, label, title }) => (
<button
key={code}
onClick={() => onLangChange(code)}
title={title}
className={cn(
'px-3 py-2 text-xs font-semibold font-mono uppercase tracking-wide',
'backdrop-blur-md bg-black/20 border border-white/10 rounded-xl',
'transition-all duration-200 hover:scale-105',
currentLang === code
? 'border-white text-white shadow-[0_0_15px_rgba(255,255,255,0.2)] bg-white/10'
: 'text-gray-500 hover:border-white/50 hover:text-white hover:bg-white/5'
)}
>
{label}
</button>
))}
</div>
);
}

View File

@@ -0,0 +1,20 @@
import { useTypewriter } from '@/hooks/useTypewriter';
interface TypewriterTextProps {
text: string;
speed?: number;
className?: string;
}
export function TypewriterText({ text, speed = 50, className }: TypewriterTextProps) {
const { displayText, isComplete } = useTypewriter(text, speed);
return (
<span className={className}>
{displayText}
{!isComplete && (
<span className="inline-block w-2.5 h-6 bg-cyan-400 ml-1 rtl:ml-0 rtl:mr-1 animate-blink translate-y-1" />
)}
</span>
);
}

View File

@@ -0,0 +1,822 @@
"use client";
import React, { useState, useMemo, useRef, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Link } from "react-router-dom";
import { cn } from "@/lib/utils";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
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,
}: {
animationSpeed?: number;
opacities?: number[];
colors?: number[][];
containerClassName?: string;
dotSize?: number;
showGradient?: boolean;
reverse?: boolean;
}) => {
return (
<div className={cn("h-full relative w-full", containerClassName)}>
<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]}
shader={`
${reverse ? "u_reverse_active" : "false"}_;
animation_speed_factor_${animationSpeed.toFixed(1)}_;
`}
center={["x", "y"]}
/>
</div>
{showGradient && (
<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 = "",
center = ["x", "y"],
}) => {
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] / 255, color[1] / 255, color[2] / 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,
type: "uniform1i",
},
};
}, [colors, opacities, totalSize, dotSize, shader]);
return (
<Shader
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;
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);
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)];
float animation_speed_factor = 0.5;
vec2 center_grid = u_resolution / 2.0 / u_total_size;
float dist_from_center = distance(center_grid, st2);
float timing_offset_intro = dist_from_center * 0.01 + (random(st2) * 0.15);
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;
opacity *= 1.0 - step(current_timing_offset, u_time * animation_speed_factor);
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;
opacity *= step(current_timing_offset, u_time * animation_speed_factor);
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;
}`}
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 = ref.current.material as THREE.ShaderMaterial;
material.uniforms.u_time.value = timestamp;
});
const getUniforms = () => {
const preparedUniforms: Record<string, unknown> = {};
for (const uniformName in uniforms) {
const uniform = 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 as number[]),
type: "3f",
};
break;
case "uniform1fv":
preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" };
break;
case "uniform3fv":
preparedUniforms[uniformName] = {
value: (uniform.value as number[][]).map((v: number[]) =>
new THREE.Vector3().fromArray(v)
),
type: "3fv",
};
break;
case "uniform2f":
preparedUniforms[uniformName] = {
value: new THREE.Vector2().fromArray(uniform.value as number[]),
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),
};
return preparedUniforms;
};
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() as Record<string, THREE.IUniform>,
glslVersion: THREE.GLSL3,
blending: THREE.CustomBlending,
blendSrc: THREE.SrcAlphaFactor,
blendDst: THREE.OneFactor,
});
return materialObject;
}, [size.width, size.height, source]);
return (
<mesh ref={ref}>
<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>
<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>
<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>
<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"></span>
</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"
></div>
<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"
></path>
</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"
></path>
</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");
}
};
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);
if (value && index < 5) {
codeInputRefs.current[index + 1]?.focus();
}
if (index === 5 && value) {
const isComplete = newCode.every((digit) => digit.length === 1);
if (isComplete) {
setReverseCanvasVisible(true);
setTimeout(() => {
setInitialCanvasVisible(false);
}, 50);
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(["", "", "", "", "", ""]);
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">
{initialCanvasVisible && (
<div className="absolute inset-0">
<CanvasRevealEffect
animationSpeed={3}
containerClassName="bg-black"
colors={[
[255, 255, 255],
[255, 255, 255],
]}
dotSize={6}
reverse={false}
/>
</div>
)}
{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>
<div className="relative z-10 flex flex-col flex-1">
<MiniNavbar />
<div className="flex flex-1 flex-col lg:flex-row ">
<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 to="#" className="underline text-white/40 hover:text-white/60 transition-colors">
MSA
</Link>
,{" "}
<Link to="#" className="underline text-white/40 hover:text-white/60 transition-colors">
Product Terms
</Link>
,{" "}
<Link to="#" className="underline text-white/40 hover:text-white/60 transition-colors">
Policies
</Link>
,{" "}
<Link to="#" className="underline text-white/40 hover:text-white/60 transition-colors">
Privacy Notice
</Link>
, and{" "}
<Link to="#" 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 to="#" className="underline text-white/40 hover:text-white/60 transition-colors">
MSA
</Link>
,{" "}
<Link to="#" className="underline text-white/40 hover:text-white/60 transition-colors">
Product Terms
</Link>
,{" "}
<Link to="#" className="underline text-white/40 hover:text-white/60 transition-colors">
Policies
</Link>
,{" "}
<Link to="#" className="underline text-white/40 hover:text-white/60 transition-colors">
Privacy Notice
</Link>
, and{" "}
<Link to="#" 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>
);
};