Files
at-mintel/packages/gatekeeper/src/components/animated-login-form.tsx
Marc Mintel 36ed26ad79
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
feat(gatekeeper): major UI upgrade - high-fidelity light theme, iridescent mouse-reactive form, and enhanced background animation
2026-02-28 21:48:03 +01:00

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>
);
}