Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m10s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m15s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m53s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
284 lines
12 KiB
TypeScript
284 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useRef } from "react";
|
|
import { motion } from "framer-motion";
|
|
import { ArrowRight, Lock, Shield, Fingerprint } from "lucide-react";
|
|
|
|
interface AnimatedLoginFormProps {
|
|
redirectUrl: string;
|
|
loginAction: (formData: FormData) => Promise<void>;
|
|
projectName: string;
|
|
}
|
|
|
|
export function AnimatedLoginForm({
|
|
redirectUrl,
|
|
loginAction,
|
|
}: AnimatedLoginFormProps) {
|
|
const [isFocused, setIsFocused] = useState(false);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [mounted, setMounted] = useState(false);
|
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
const beamRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Mouse tracking refs (no re-render)
|
|
const mouse = useRef({ x: 0, y: 0 });
|
|
const angle = useRef(0);
|
|
const tilt = useRef({ x: 0, y: 0 });
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
}, []);
|
|
|
|
// Single rAF loop: iridescent border + perspective tilt
|
|
useEffect(() => {
|
|
let animId: number;
|
|
|
|
const onMouseMove = (e: MouseEvent) => {
|
|
mouse.current = { x: e.clientX, y: e.clientY };
|
|
};
|
|
window.addEventListener("mousemove", onMouseMove);
|
|
|
|
const animate = () => {
|
|
if (!wrapperRef.current || !beamRef.current) {
|
|
animId = requestAnimationFrame(animate);
|
|
return;
|
|
}
|
|
|
|
const rect = wrapperRef.current.getBoundingClientRect();
|
|
const cx = rect.left + rect.width / 2;
|
|
const cy = rect.top + rect.height / 2;
|
|
const dx = mouse.current.x - cx;
|
|
const dy = mouse.current.y - cy;
|
|
|
|
// Angle from form center to mouse → positions the bright highlight
|
|
const targetAngle = (Math.atan2(dy, dx) * 180) / Math.PI;
|
|
|
|
// Lerp angle smoothly (shortest path)
|
|
let diff = targetAngle - angle.current;
|
|
while (diff > 180) diff -= 360;
|
|
while (diff < -180) diff += 360;
|
|
angle.current += diff * 0.06;
|
|
|
|
// Intensity: slightly stronger on focus
|
|
const intensity = isFocused ? 1 : 0.7;
|
|
|
|
// Mouse-aligned iridescent conic gradient
|
|
// The "hotspot" (brightest white) faces the mouse
|
|
beamRef.current.style.background = `conic-gradient(from ${angle.current}deg at 50% 50%,
|
|
rgba(255,255,255,${1.0 * intensity}) 0deg,
|
|
rgba(200,210,255,${0.8 * intensity}) 20deg,
|
|
rgba(255,200,230,${0.7 * intensity}) 45deg,
|
|
rgba(150,160,180,${0.6 * intensity}) 80deg,
|
|
rgba(40,40,50,${0.5 * intensity}) 160deg,
|
|
rgba(20,20,30,${0.4 * intensity}) 200deg,
|
|
rgba(140,150,170,${0.5 * intensity}) 280deg,
|
|
rgba(210,225,255,${0.7 * intensity}) 320deg,
|
|
rgba(255,255,255,${1.0 * intensity}) 360deg)`;
|
|
|
|
// Subtle perspective tilt — max ±4deg
|
|
const maxTilt = 4;
|
|
const normX = dx / (rect.width * 2);
|
|
const normY = dy / (rect.height * 2);
|
|
const targetTiltY = normX * maxTilt;
|
|
const targetTiltX = -normY * maxTilt;
|
|
tilt.current.x += (targetTiltX - tilt.current.x) * 0.08;
|
|
tilt.current.y += (targetTiltY - tilt.current.y) * 0.08;
|
|
|
|
wrapperRef.current.style.transform = `perspective(800px) rotateX(${tilt.current.x}deg) rotateY(${tilt.current.y}deg)`;
|
|
|
|
animId = requestAnimationFrame(animate);
|
|
};
|
|
|
|
animId = requestAnimationFrame(animate);
|
|
return () => {
|
|
window.removeEventListener("mousemove", onMouseMove);
|
|
cancelAnimationFrame(animId);
|
|
};
|
|
}, [isFocused]);
|
|
|
|
const handleSubmit = async (formData: FormData) => {
|
|
setIsSubmitting(true);
|
|
try {
|
|
await loginAction(formData);
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30, scale: 0.95 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
transition={{ duration: 0.8, ease: [0.23, 1, 0.32, 1], delay: 0.3 }}
|
|
className="relative"
|
|
style={{ willChange: "transform" }}
|
|
>
|
|
{/* Outer wrapper for tilt (refs need non-motion div) */}
|
|
<div
|
|
ref={wrapperRef}
|
|
style={{ willChange: "transform", transformStyle: "preserve-3d" }}
|
|
className="relative"
|
|
>
|
|
{/* ── Always-on iridescent beam border ── */}
|
|
<div className="absolute -inset-[1.5px] rounded-[28px] z-0 overflow-hidden">
|
|
{/* Sharp edge layer */}
|
|
<div
|
|
ref={beamRef}
|
|
className="absolute inset-0 rounded-[28px]"
|
|
style={{ filter: "blur(0.4px)" }}
|
|
/>
|
|
{/* Soft glow bloom */}
|
|
<div
|
|
className="absolute inset-0 rounded-[28px]"
|
|
style={{
|
|
background: "inherit",
|
|
filter: "blur(15px)",
|
|
opacity: 0.5,
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* ── Glassmorphism card — high-fidelity glossy light ── */}
|
|
<div
|
|
className="relative z-10 bg-white/70 backdrop-blur-3xl rounded-3xl p-7 sm:p-9"
|
|
style={{
|
|
boxShadow:
|
|
/* Razor-sharp inner border highlight */ "inset 0 0 0 1px rgba(255,255,255,0.7), " +
|
|
/* Top gloss edge */ "inset 0 1.5px 0.5px rgba(255,255,255,1), " +
|
|
/* Secondary soft top gloss */ "inset 0 4px 10px rgba(255,255,255,0.4), " +
|
|
/* Bottom inner shadow */ "inset 0 -1px 1px rgba(0,0,0,0.05), " +
|
|
/* Outer drop shadows for depth */ "0 25px 50px -12px rgba(0,0,0,0.08), 0 4px 8px rgba(0,0,0,0.02)",
|
|
}}
|
|
>
|
|
{/* Subtle surface "sheen" gradient */}
|
|
<div className="absolute inset-0 rounded-3xl bg-gradient-to-br from-white/40 via-transparent to-black/[0.02] pointer-events-none" />
|
|
|
|
{/* Shield icon header */}
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ delay: 0.5, duration: 0.6 }}
|
|
className="flex justify-center mb-6"
|
|
>
|
|
<div className="relative">
|
|
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-black/[0.04] to-black/[0.08] border border-black/[0.06] flex items-center justify-center backdrop-blur-sm">
|
|
<Shield className="w-5 h-5 text-black/40" />
|
|
</div>
|
|
{mounted && (
|
|
<div className="absolute -inset-2 rounded-2xl border border-black/[0.04] animate-ping opacity-30" />
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
|
|
<form
|
|
action={handleSubmit}
|
|
className="space-y-5 relative z-10"
|
|
onFocus={() => setIsFocused(true)}
|
|
onBlur={(e) => {
|
|
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
setIsFocused(false);
|
|
}
|
|
}}
|
|
>
|
|
<input type="hidden" name="redirect" value={redirectUrl} />
|
|
|
|
<div className="space-y-3">
|
|
{/* Email Input */}
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: 0.6, duration: 0.5 }}
|
|
className="relative group"
|
|
>
|
|
<Fingerprint className="absolute left-4 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-black/20 group-focus-within:text-black/50 transition-colors duration-300 pointer-events-none" />
|
|
<input
|
|
type="email"
|
|
name="email"
|
|
placeholder="Identity (optional)"
|
|
onFocus={() => setIsFocused(true)}
|
|
className="w-full bg-black/[0.02] border border-black/[0.06] rounded-2xl pl-11 pr-4 py-3.5 focus:outline-none focus:border-black/15 focus:bg-white/80 transition-all duration-300 text-[11px] font-sans font-medium tracking-[0.08em] placeholder:text-black/25 placeholder:normal-case placeholder:tracking-normal text-black/70"
|
|
/>
|
|
<div className="absolute bottom-0 left-4 right-4 h-px bg-gradient-to-r from-transparent via-black/0 to-transparent group-focus-within:via-black/10 transition-all duration-500" />
|
|
</motion.div>
|
|
|
|
{/* Password Input */}
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: 0.7, duration: 0.5 }}
|
|
className="relative group"
|
|
>
|
|
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-black/20 group-focus-within:text-black/50 transition-colors duration-300 pointer-events-none" />
|
|
<input
|
|
type="password"
|
|
name="password"
|
|
required
|
|
autoFocus
|
|
autoComplete="current-password"
|
|
placeholder="Access code"
|
|
onFocus={() => setIsFocused(true)}
|
|
className="w-full bg-black/[0.02] border border-black/[0.06] rounded-2xl pl-11 pr-4 py-3.5 focus:outline-none focus:border-black/15 focus:bg-white/80 transition-all duration-300 text-[13px] font-sans font-medium tracking-[0.15em] placeholder:text-black/25 placeholder:tracking-normal placeholder:text-[11px] placeholder:font-normal text-black/80"
|
|
/>
|
|
<div className="absolute bottom-0 left-4 right-4 h-px bg-gradient-to-r from-transparent via-black/0 to-transparent group-focus-within:via-black/10 transition-all duration-500" />
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Submit Button */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.8, duration: 0.5 }}
|
|
>
|
|
<motion.button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
whileHover={{ scale: 1.01 }}
|
|
whileTap={{ scale: 0.98 }}
|
|
className={`relative w-full py-4 rounded-2xl text-[10px] font-bold tracking-[0.3em] uppercase flex items-center justify-center overflow-hidden transition-all duration-300 ${
|
|
isSubmitting
|
|
? "bg-black/5 text-black/25 cursor-not-allowed"
|
|
: "bg-black text-white hover:bg-black/85 shadow-lg shadow-black/10"
|
|
}`}
|
|
>
|
|
{!isSubmitting && (
|
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full hover:translate-x-full transition-transform duration-700" />
|
|
)}
|
|
{isSubmitting ? (
|
|
<span className="flex items-center gap-3 relative z-10">
|
|
<motion.div
|
|
animate={{ rotate: 360 }}
|
|
transition={{
|
|
repeat: Infinity,
|
|
duration: 1,
|
|
ease: "linear",
|
|
}}
|
|
className="w-4 h-4 border-2 border-white/20 border-t-white/70 rounded-full"
|
|
/>
|
|
Authenticating...
|
|
</span>
|
|
) : (
|
|
<span className="flex items-center gap-3 relative z-10">
|
|
Unlock Access
|
|
<ArrowRight className="w-4 h-4" />
|
|
</span>
|
|
)}
|
|
</motion.button>
|
|
</motion.div>
|
|
</form>
|
|
|
|
{/* Security badge */}
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 1.0, duration: 0.5 }}
|
|
className="mt-5 flex items-center justify-center gap-2 text-[8px] font-sans font-semibold text-black/25 uppercase tracking-[0.3em]"
|
|
>
|
|
<div className="w-1 h-1 rounded-full bg-black/20 animate-pulse" />
|
|
Encrypted Connection
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|