Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions apps/www/src/app/auth/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
"use client";

import { useRef, useState } from "react";
import Link from "next/link";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { Mail, ArrowRight, ArrowLeft, KeyRound } from "lucide-react";
import { Button } from "@/components/ui/button";
import { AuthCard } from "@/components/auth/AuthCard";
import { AuthInput } from "@/components/auth/AuthInput";

gsap.registerPlugin(useGSAP);

// Stub: replace with real auth logic
async function requestPasswordReset(_email: string): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 1200));
}

export default function ForgotPasswordPage() {
const cardRef = useRef<HTMLDivElement>(null);
const [email, setEmail] = useState("");
const [emailError, setEmailError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [serverError, setServerError] = useState("");

useGSAP(
() => {
const tl = gsap.timeline();
tl.fromTo(
cardRef.current,
{ opacity: 0, y: 32, scale: 0.97 },
{ opacity: 1, y: 0, scale: 1, duration: 0.8, ease: "expo.out" }
);
tl.from(
".auth-item",
{
opacity: 0,
y: 12,
stagger: 0.07,
duration: 0.5,
ease: "power2.out",
},
"-=0.4"
);
},
{ scope: cardRef }
);

async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setServerError("");
if (!email) {
setEmailError("Email is required");
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
setEmailError("Enter a valid email address");
gsap.fromTo(
".auth-field-error",
{ opacity: 0, x: -6 },
{ opacity: 1, x: 0, duration: 0.3, ease: "power2.out" }
);
return;
}
setEmailError("");
setIsLoading(true);
try {
await requestPasswordReset(email);
setIsSuccess(true);
gsap.fromTo(
".auth-success",
{ opacity: 0, scale: 0.9 },
{ opacity: 1, scale: 1, duration: 0.5, ease: "back.out(1.7)" }
);
} catch {
setServerError("Something went wrong. Please try again.");
gsap.fromTo(
".auth-server-error",
{ opacity: 0, y: -8 },
{ opacity: 1, y: 0, duration: 0.4, ease: "power2.out" }
);
} finally {
setIsLoading(false);
}
}

return (
<div ref={cardRef} className="w-full max-w-md">
<AuthCard>
{/* Back link */}
<Link
href="/auth/login"
className="auth-item inline-flex items-center gap-2 font-mono text-[11px] uppercase tracking-widest text-zinc-600 hover:text-white transition-colors mb-6"
>
<ArrowLeft size={14} />
Back to sign in
</Link>

{/* Header */}
<div className="flex items-center gap-3 mb-8 auth-item">
<div className="w-10 h-10 rounded-xl bg-white/5 flex items-center justify-center border border-white/10 flex-shrink-0">
<KeyRound size={20} className="text-zinc-400" />
</div>
<div>
<h1 className="font-display font-bold text-xl sm:text-2xl text-white leading-tight">
Forgot password?
</h1>
<p className="font-mono text-[10px] sm:text-[11px] uppercase tracking-widest text-zinc-600">
We&apos;ll send a reset link
</p>
</div>
</div>

{serverError && (
<div className="auth-server-error mb-5 p-3 sm:p-4 rounded-xl border border-red/20 bg-red/5">
<p className="font-mono text-[11px] sm:text-xs text-red">{serverError}</p>
</div>
)}

{isSuccess ? (
<div className="auth-success text-center py-6">
<div className="w-14 h-14 rounded-2xl bg-blue/10 border border-blue/20 flex items-center justify-center mx-auto mb-4">
<Mail size={28} className="text-blue" />
</div>
<h2 className="font-display font-bold text-lg text-white mb-2">
Check your inbox
</h2>
<p className="font-mono text-xs text-zinc-500 max-w-xs mx-auto mb-6">
If an account exists for{" "}
<span className="text-zinc-300">{email}</span>, you&apos;ll receive
a password reset link shortly.
</p>
<div className="flex flex-col gap-3">
<Button
variant="outline"
className="w-full h-11 rounded-xl border-white/10 text-zinc-300 hover:text-white hover:bg-white/5 font-mono text-xs uppercase tracking-widest"
onClick={() => {
setIsSuccess(false);
setEmail("");
}}
>
Try another email
</Button>
<Link
href="/auth/login"
className="text-center font-mono text-[11px] uppercase tracking-widest text-zinc-600 hover:text-white transition-colors"
>
Return to sign in
</Link>
</div>
</div>
) : (
<form className="space-y-5" onSubmit={handleSubmit} noValidate>
<div className="auth-item">
<p className="font-serif text-sm sm:text-base text-zinc-400 mb-5 leading-relaxed">
Enter the email address associated with your account and
we&apos;ll send you a link to reset your password.
</p>
<AuthInput
label="Email address"
type="email"
placeholder="you@company.com"
icon={<Mail size={18} />}
value={email}
onChange={(e) => setEmail(e.target.value)}
error={emailError}
autoComplete="email"
aria-label="Email address"
/>
</div>

<div className="auth-item pt-2">
<Button
type="submit"
variant="primary"
className="w-full h-12 sm:h-14 rounded-xl sm:rounded-2xl text-sm sm:text-base group"
disabled={isLoading}
>
{isLoading ? (
<span className="flex items-center gap-2">
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Sending link…
</span>
) : (
<>
Send Reset Link
<ArrowRight
size={18}
className="transition-transform group-hover:translate-x-1"
/>
</>
)}
</Button>
</div>
</form>
)}
</AuthCard>
</div>
);
}
102 changes: 102 additions & 0 deletions apps/www/src/app/auth/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client";

import Link from "next/link";
import Image from "next/image";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";

gsap.registerPlugin(useGSAP);

export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
const containerRef = useRef<HTMLDivElement>(null);

useGSAP(
() => {
gsap.fromTo(
".auth-bg-orb",
{ opacity: 0, scale: 0.8 },
{ opacity: 1, scale: 1, duration: 2, ease: "power2.out", stagger: 0.3 }
);
},
{ scope: containerRef }
);

return (
<div
ref={containerRef}
className="min-h-screen bg-black flex flex-col font-display overflow-hidden relative"
>
{/* Background decorative orbs */}
<div className="auth-bg-orb absolute top-[-20%] left-[-10%] w-[600px] h-[600px] rounded-full bg-blue/5 blur-[120px] pointer-events-none" />
<div className="auth-bg-orb absolute bottom-[-20%] right-[-10%] w-[500px] h-[500px] rounded-full bg-teal/5 blur-[100px] pointer-events-none" />
<div className="auth-bg-orb absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] rounded-full bg-blue/3 blur-[150px] pointer-events-none" />

{/* Grid pattern overlay */}
<div
className="absolute inset-0 pointer-events-none opacity-[0.03]"
style={{
backgroundImage: `linear-gradient(rgba(255,255,255,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.5) 1px, transparent 1px)`,
backgroundSize: "60px 60px",
}}
/>

{/* Header */}
<header className="relative z-10 flex items-center justify-between px-6 sm:px-10 h-16 sm:h-20">
<Link href="/" className="flex items-center">
<Image
src="/logo.svg"
alt="Useroutr"
width={110}
height={32}
className="w-auto h-6 sm:h-7"
/>
</Link>
<nav className="flex items-center gap-6">
<Link
href="/"
className="font-mono text-[11px] uppercase tracking-widest text-zinc-600 hover:text-white transition-colors"
>
Home
</Link>
<Link
href="https://thirtn.mintlify.app/"
className="font-mono text-[11px] uppercase tracking-widest text-zinc-600 hover:text-white transition-colors"
>
Docs
</Link>
</nav>
</header>

{/* Main content */}
<main className="relative z-10 flex-1 flex items-center justify-center px-4 py-10">
{children}
</main>

{/* Footer */}
<footer className="relative z-10 flex items-center justify-center gap-6 px-6 h-14 border-t border-white/5">
<Link
href="#"
className="font-mono text-[10px] uppercase tracking-widest text-zinc-700 hover:text-zinc-400 transition-colors"
>
Privacy
</Link>
<div className="w-px h-3 bg-white/10" />
<Link
href="#"
className="font-mono text-[10px] uppercase tracking-widest text-zinc-700 hover:text-zinc-400 transition-colors"
>
Terms
</Link>
<div className="w-px h-3 bg-white/10" />
<span className="font-mono text-[10px] uppercase tracking-widest text-zinc-800">
© 2025 Useroutr Labs
</span>
</footer>
</div>
);
}
Loading
Loading