feat(gatekeeper): major UI upgrade - high-fidelity light theme, iridescent mouse-reactive form, and enhanced background animation
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

This commit is contained in:
2026-02-28 21:48:03 +01:00
parent 4e72a0baac
commit 36ed26ad79
10 changed files with 1190 additions and 98 deletions

View File

@@ -8,7 +8,7 @@
}
body {
@apply bg-white text-slate-800 font-serif antialiased selection:bg-slate-900 selection:text-white;
@apply bg-[#f5f5f7] text-black/80 font-serif antialiased selection:bg-black/10 selection:text-black;
line-height: 1.6;
}
@@ -18,15 +18,15 @@
h4,
h5,
h6 {
@apply font-sans font-bold text-slate-900 tracking-tighter;
@apply font-sans font-bold text-black tracking-tighter;
}
p {
@apply mb-4 text-base leading-relaxed text-slate-700;
@apply mb-4 text-base leading-relaxed text-black/50;
}
a {
@apply text-slate-900 hover:text-slate-700 transition-colors no-underline;
@apply text-black/50 hover:text-black transition-colors no-underline;
}
}
@@ -36,34 +36,58 @@
}
.btn {
@apply inline-flex items-center justify-center px-6 py-3 border border-slate-200 bg-white text-slate-600 font-sans font-bold text-sm uppercase tracking-widest rounded-full transition-all duration-500 ease-industrial hover:border-slate-400 hover:text-slate-900 hover:bg-slate-50 hover:-translate-y-0.5 hover:shadow-xl hover:shadow-slate-100 active:translate-y-0 active:shadow-sm;
@apply inline-flex items-center justify-center px-6 py-3 border border-black/10 bg-white text-black/60 font-sans font-bold text-sm uppercase tracking-widest rounded-full transition-all duration-500 ease-industrial hover:border-black/20 hover:text-black hover:bg-white hover:-translate-y-0.5 hover:shadow-xl hover:shadow-black/5 active:translate-y-0 active:shadow-sm;
}
.btn-primary {
@apply border-slate-900 text-slate-900 hover:bg-slate-900 hover:text-white;
@apply border-black bg-black text-white hover:bg-black/85 hover:text-white;
}
}
/* Custom scrollbar */
/* Custom scrollbar - light theme */
::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
background: #f5f5f7;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
background: #d1d1d6;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
background: #b0b0b8;
}
/* Animations */
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shake {
0%,
100% {
@@ -79,6 +103,15 @@
}
}
.animate-fade-in {
animation: fade-in 0.8s ease-out forwards;
}
.animate-slide-up {
animation: slide-up 0.8s ease-out 0.2s forwards;
opacity: 0;
}
.animate-shake {
animation: shake 0.2s ease-in-out 0s 2;
}

View File

@@ -13,6 +13,17 @@ const newsreader = Newsreader({
export const metadata: Metadata = {
title: "Gatekeeper | Access Control",
description: "Mintel Infrastructure Protection",
openGraph: {
title: "Gatekeeper | Access Control",
description: "Mintel Infrastructure Protection",
siteName: "Mintel Gatekeeper",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Gatekeeper | Access Control",
description: "Mintel Infrastructure Protection",
},
};
export default function RootLayout({

View File

@@ -1,7 +1,9 @@
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { ArrowRight, ShieldCheck } from "lucide-react";
import { ShieldCheck } from "lucide-react";
import Image from "next/image";
import { GateScene } from "../../components/gate-scene";
import { AnimatedLoginForm } from "../../components/animated-login-form";
interface LoginPageProps {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
@@ -17,8 +19,8 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
async function login(formData: FormData) {
"use server";
const email = (formData.get("email") as string || "").trim();
const password = (formData.get("password") as string || "").trim();
const email = ((formData.get("email") as string) || "").trim();
const password = ((formData.get("password") as string) || "").trim();
const expectedCode = process.env.GATEKEEPER_PASSWORD || "mintel";
const adminEmail = process.env.DIRECTUS_ADMIN_EMAIL;
@@ -116,7 +118,9 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
}
if (userIdentity) {
console.log(`[Login] Success: ${userIdentity} | Redirect: ${targetRedirect}`);
console.log(
`[Login] Success: ${userIdentity} | Redirect: ${targetRedirect}`,
);
const cookieStore = await cookies();
// Store identity in the cookie (simplified for now, ideally signed)
const sessionValue = JSON.stringify({
@@ -127,7 +131,9 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
const isDev = process.env.NODE_ENV === "development";
console.log(`[Login] Setting Cookie: ${authCookieName} | Domain: ${cookieDomain || "Default"}`);
console.log(
`[Login] Setting Cookie: ${authCookieName} | Domain: ${cookieDomain || "Default"}`,
);
cookieStore.set(authCookieName, sessionValue, {
httpOnly: true,
@@ -145,101 +151,81 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
}
return (
<div className="min-h-screen flex items-center justify-center relative bg-white font-serif antialiased overflow-hidden">
{/* Background Decor - Signature mintel.me style */}
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] scale-[1.01]"
style={{
backgroundImage: `linear-gradient(to right, #000 1px, transparent 1px), linear-gradient(to bottom, #000 1px, transparent 1px)`,
backgroundSize: "clamp(30px, 8vw, 40px) clamp(30px, 8vw, 40px)",
}}
/>
<div className="min-h-screen flex items-center justify-center relative bg-[#f5f5f7] font-serif antialiased overflow-hidden selection:bg-black/10 selection:text-black">
{/* 3D Digital Gate Background */}
<GateScene />
<main className="relative z-10 w-full max-w-sm px-8 sm:px-6">
<div className="space-y-12 sm:space-y-16 animate-fade-in">
{/* Top Icon Box - Signature mintel.me Black Square */}
<main className="relative z-10 w-full max-w-[380px] px-6 sm:px-4 pb-24 sm:pb-32 pointer-events-auto">
<div className="space-y-10 animate-fade-in">
{/* Top Icon Box */}
<div className="flex justify-center">
<div className="w-16 h-16 bg-black rounded-xl flex items-center justify-center shadow-xl shadow-slate-100 hover:scale-105 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] rotate-2 hover:rotate-0">
<a
href="https://mintel.me"
target="_blank"
rel="noopener noreferrer"
className="w-14 h-14 bg-white rounded-2xl flex items-center justify-center shadow-lg shadow-black/[0.06] hover:scale-105 hover:shadow-black/10 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] border border-black/[0.06] hover:border-black/10"
>
<Image
src="/gatekeeper/icon-white.svg"
alt="Mintel"
width={32}
height={32}
className="w-8 h-8"
style={{ filter: "invert(1)" }}
width={28}
height={28}
className="w-7 h-7 opacity-80"
unoptimized
/>
</div>
</a>
</div>
<div className="space-y-12 animate-slide-up">
<div className="text-center space-y-4">
<h1 className="text-xs font-sans font-bold uppercase tracking-[0.4em] text-slate-900 border-b border-slate-50 pb-4 inline-block mx-auto min-w-[200px]">
{projectName} <span className="text-slate-300">Gatekeeper</span>
<div className="space-y-8 animate-slide-up">
<div className="text-center space-y-3">
<h1 className="text-[11px] font-sans font-bold uppercase tracking-[0.5em] text-black/80 pb-3 inline-block mx-auto min-w-[220px]">
{projectName} <span className="text-black/30">Gatekeeper</span>
</h1>
<p className="text-[10px] text-slate-400 font-sans uppercase tracking-widest italic flex items-center justify-center gap-2">
<span className="w-1 h-1 bg-slate-200 rounded-full" />
Infrastructure Protection
<span className="w-1 h-1 bg-slate-200 rounded-full" />
</p>
<div className="flex items-center justify-center gap-4">
<div className="h-px w-8 bg-gradient-to-r from-transparent to-black/10" />
<p className="text-[8px] text-black/30 font-sans uppercase tracking-[0.35em] font-semibold">
Infrastructure Protection
</p>
<div className="h-px w-8 bg-gradient-to-l from-transparent to-black/10" />
</div>
</div>
{error && (
<div className="bg-red-50 text-red-600 px-5 py-3 rounded-2xl text-[9px] font-sans font-bold uppercase tracking-widest flex items-center gap-3 border border-red-100 animate-shake">
<ShieldCheck className="w-4 h-4" />
<div className="bg-red-50 backdrop-blur-md text-red-600 px-5 py-4 rounded-2xl text-[9px] font-sans font-bold uppercase tracking-widest flex items-center gap-3 border border-red-200 animate-shake">
<ShieldCheck className="w-4 h-4 text-red-500/70" />
<span>Access Denied. Try Again.</span>
</div>
)}
<form action={login} className="space-y-4">
<input type="hidden" name="redirect" value={redirectUrl} />
{/* The Animated Framer Motion Form */}
<AnimatedLoginForm
redirectUrl={redirectUrl}
loginAction={login}
projectName={projectName}
/>
<div className="space-y-2">
<div className="relative group">
<input
type="email"
name="email"
placeholder="EMAIL (OPTIONAL)"
className="w-full bg-slate-50/50 border border-slate-200 rounded-2xl px-6 py-4 focus:outline-none focus:border-slate-900 focus:bg-white transition-all text-[10px] font-sans font-bold tracking-[0.2em] uppercase placeholder:text-slate-300 shadow-sm shadow-slate-50"
/>
</div>
<div className="relative group">
<input
type="password"
name="password"
required
autoFocus
autoComplete="current-password"
placeholder="ACCESS CODE"
className="w-full bg-slate-50/50 border border-slate-200 rounded-2xl px-6 py-4 focus:outline-none focus:border-slate-900 focus:bg-white transition-all text-sm font-sans font-bold tracking-[0.3em] uppercase placeholder:text-slate-300 placeholder:tracking-widest shadow-sm shadow-slate-50"
/>
</div>
</div>
<button
type="submit"
className="btn btn-primary w-full py-5 rounded-2xl text-[10px] shadow-lg shadow-slate-100 flex items-center justify-center"
{/* Bottom Section */}
<div className="pt-4 sm:pt-6 flex flex-col items-center gap-5">
<div className="h-px w-16 bg-gradient-to-r from-transparent via-black/10 to-transparent" />
<a
href="https://mintel.me"
target="_blank"
rel="noopener noreferrer"
className="opacity-30 transition-opacity hover:opacity-60"
>
Unlock Access
<ArrowRight className="ml-3 w-3 h-3 group-hover:translate-x-1 transition-transform" />
</button>
</form>
{/* Bottom Section - Full Branding Parity */}
<div className="pt-12 sm:pt-20 flex flex-col items-center gap-6 sm:gap-8">
<div className="h-px w-8 bg-slate-100" />
<div className="opacity-80 transition-opacity hover:opacity-100">
<Image
src="/gatekeeper/logo-black.svg"
src="/gatekeeper/logo-white.svg"
alt={projectName}
width={140}
height={40}
className="h-7 sm:h-auto grayscale contrast-125 w-auto"
width={120}
height={36}
className="h-5 sm:h-6 w-auto"
style={{ filter: "invert(1)" }}
unoptimized
/>
</div>
<p className="text-[8px] font-sans font-bold text-slate-300 uppercase tracking-[0.4em] sm:tracking-[0.5em] text-center">
&copy; 2026 MINTEL
</a>
<p className="text-[7px] font-sans font-semibold text-black/25 uppercase tracking-[0.5em] text-center">
&copy; {new Date().getFullYear()} MINTEL
</p>
</div>
</div>

View File

@@ -0,0 +1,126 @@
import { ImageResponse } from "next/og";
export const runtime = "edge";
// Image metadata
export const alt = "Gatekeeper Infrastructure Protection";
export const size = {
width: 1200,
height: 630,
};
export const contentType = "image/png";
export default async function Image() {
const projectName = process.env.PROJECT_NAME || "MINTEL";
return new ImageResponse(
<div
style={{
background: "linear-gradient(to bottom, #020617, #0f172a)",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
position: "relative",
fontFamily: "Inter, sans-serif",
}}
>
{/* Subtle Background Pattern matching the industrial look */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundImage:
"radial-gradient(circle at 50% 50%, #334155 1px, transparent 1px)",
backgroundSize: "40px 40px",
opacity: 0.1,
}}
/>
{/* Central Card Element */}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "rgba(15, 23, 42, 0.6)",
border: "1px solid rgba(51, 65, 85, 0.4)",
borderRadius: "32px",
padding: "80px",
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
}}
>
{/* Top Icon Box */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100px",
height: "100px",
background: "#000",
borderRadius: "24px",
border: "2px solid #334155",
boxShadow: "0 20px 40px rgba(0,0,0,0.8)",
marginBottom: "40px",
transform: "rotate(2deg)",
}}
>
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
</div>
{/* Project Name & Typography */}
<div
style={{
display: "flex",
fontSize: "64px",
fontWeight: 900,
letterSpacing: "0.2em",
color: "white",
textTransform: "uppercase",
marginBottom: "16px",
}}
>
{projectName}{" "}
<span style={{ color: "#64748b", marginLeft: "10px" }}>
GATEKEEPER
</span>
</div>
<div
style={{
display: "flex",
fontSize: "24px",
fontWeight: 600,
letterSpacing: "0.4em",
color: "#94a3b8",
textTransform: "uppercase",
}}
>
Infrastructure Protection
</div>
</div>
</div>,
{
...size,
},
);
}

View File

@@ -0,0 +1,283 @@
"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>
);
}

View File

@@ -0,0 +1,127 @@
"use client";
import { useEffect, useRef } from "react";
export function GateScene() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const mouse = useRef({ x: -1000, y: -1000 });
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const FONT_SIZE = 13;
const COL_GAP = 18;
const ROW_GAP = 20;
interface Cell {
char: string;
alpha: number;
targetAlpha: number;
speed: number;
nextChange: number;
}
let cells: Cell[][] = [];
let cols = 0;
let rows = 0;
let animId: number;
const init = () => {
cols = Math.ceil(canvas.width / COL_GAP);
rows = Math.ceil(canvas.height / ROW_GAP);
cells = Array.from({ length: cols }, () =>
Array.from({ length: rows }, () => ({
char: Math.random() > 0.5 ? "1" : "0",
alpha: Math.random() * 0.08,
targetAlpha: Math.random() * 0.15,
speed: 0.008 + Math.random() * 0.02,
nextChange: Math.floor(Math.random() * 80),
})),
);
};
const onMouseMove = (e: MouseEvent) => {
mouse.current = { x: e.clientX, y: e.clientY };
};
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
init();
};
let frame = 0;
const draw = () => {
frame++;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = `${FONT_SIZE}px 'Courier New', monospace`;
for (let c = 0; c < cols; c++) {
for (let r = 0; r < rows; r++) {
const cell = cells[c][r];
const x = c * COL_GAP;
const y = r * ROW_GAP + FONT_SIZE;
// Mouse proximity influence
const dx = mouse.current.x - x;
const dy = mouse.current.y - y;
const distSq = dx * dx + dy * dy;
const proximity = Math.max(0, 1 - Math.sqrt(distSq) / 250);
// Nudge alpha toward target
cell.alpha += (cell.targetAlpha - cell.alpha) * cell.speed;
// More aggressive random behavior
if (frame >= cell.nextChange) {
cell.targetAlpha = Math.random() * 0.25; // Higher max alpha
cell.speed = 0.01 + Math.random() * 0.03; // Faster transitions
cell.nextChange = frame + 20 + Math.floor(Math.random() * 80); // More frequent changes
// Higher flip probability near mouse
const flipProb = 0.3 + proximity * 0.5;
if (Math.random() < flipProb) {
cell.char = cell.char === "0" ? "1" : "0";
}
}
const a = Math.min(0.4, cell.alpha + proximity * 0.35);
if (a < 0.01) continue;
// Dark chars on light background
ctx.fillStyle = `rgba(0, 0, 0, ${a})`;
ctx.fillText(cell.char, x, y);
}
}
animId = requestAnimationFrame(draw);
};
resize();
window.addEventListener("resize", resize);
window.addEventListener("mousemove", onMouseMove);
animId = requestAnimationFrame(draw);
return () => {
cancelAnimationFrame(animId);
window.removeEventListener("resize", resize);
window.removeEventListener("mousemove", onMouseMove);
};
}, []);
return (
<div className="absolute inset-0 pointer-events-none z-0 bg-[#f5f5f7]">
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
<div
className="absolute inset-0 pointer-events-none"
style={{
background:
"radial-gradient(ellipse 55% 65% at 50% 50%, rgba(245,245,247,0.7) 0%, transparent 100%)",
}}
/>
</div>
);
}