Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
307 changes: 260 additions & 47 deletions components/AuthCard.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
"use client";

import { useState, useCallback } from "react";
import { useState, useCallback, useEffect } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import { signInWithGoogle, signInWithGitHub } from "../src/lib/firebase/auth";
import { signInWithGoogle, signInWithGitHub, signInWithEmail, signUpWithEmail, resetPassword } from "../src/lib/firebase/auth";
import { useAuth } from "../src/lib/firebase/AuthContext";
import { User } from "firebase/auth";
import { Spinner } from "./ui/Spinner";

// Sync Firebase user with MongoDB - throws on failure
async function syncUserWithDB(user: User, provider: 'google' | 'github') {
async function syncUserWithDB(user: User, provider: 'google' | 'github' | 'email') {
// Need to get fresh token after sign-in
const token = await user.getIdToken();

Expand Down Expand Up @@ -40,16 +41,33 @@ interface AuthCardProps {

export default function AuthCard({ mode = 'login' }: AuthCardProps) {
const router = useRouter();
const { user: currentUser, isLoading: authLoading } = useAuth();
const [isLoadingGoogle, setIsLoadingGoogle] = useState(false);
const [isLoadingGitHub, setIsLoadingGitHub] = useState(false);
const [isLoadingEmail, setIsLoadingEmail] = useState(false);
const [signInError, setSignInError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);

// Email/password form state
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [displayName, setDisplayName] = useState('');
const [showResetPassword, setShowResetPassword] = useState(false);

// If user is already logged in, redirect to dashboard
useEffect(() => {
if (!authLoading && currentUser) {
router.push('/dashboard');
}
}, [authLoading, currentUser, router]);

const handleProviderSignIn = useCallback(async (
signInFn: () => Promise<User | null>,
setLoading: (v: boolean) => void,
provider: 'google' | 'github'
) => {
setSignInError(null);
setSuccessMessage(null);
setLoading(true);
try {
const user = await signInFn();
Expand All @@ -70,17 +88,93 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) {
}
}, [router]);

const isLoading = isLoadingGoogle || isLoadingGitHub;
const handleEmailSubmit = useCallback(async (e: React.FormEvent) => {
e.preventDefault();
setSignInError(null);
setSuccessMessage(null);
setIsLoadingEmail(true);

try {
let user: User | null = null;

if (mode === 'signup') {
if (!displayName.trim()) {
setSignInError('Please enter your name.');
return;
}
user = await signUpWithEmail(email, password, displayName.trim());
} else {
user = await signInWithEmail(email, password);
}

if (!user) {
setSignInError('Authentication failed. Please try again.');
return;
}

await syncUserWithDB(user, 'email');
router.push('/dashboard');
} catch (error) {
setSignInError(
error instanceof Error
? error.message
: 'Authentication failed. Please try again.'
);
} finally {
setIsLoadingEmail(false);
}
}, [mode, email, password, displayName, router]);

const handlePasswordReset = useCallback(async (e: React.FormEvent) => {
e.preventDefault();
setSignInError(null);
setSuccessMessage(null);

if (!email.trim()) {
setSignInError('Please enter your email address.');
return;
}

try {
await resetPassword(email);
setSuccessMessage('Password reset email sent! Check your inbox.');
setShowResetPassword(false);
} catch (error) {
setSignInError(
error instanceof Error
? error.message
: 'Failed to send reset email.'
);
}
}, [email]);

const isLoading = isLoadingGoogle || isLoadingGitHub || isLoadingEmail;

// Show nothing while checking auth (prevents flash)
if (authLoading) {
return (
<div className="w-full max-w-md rounded-2xl bg-[#141022]/80 backdrop-blur border border-white/10 p-8 shadow-2xl flex items-center justify-center min-h-[300px]">
<Spinner label="Loading" />
</div>
);
}

// If already logged in, don't render the card (redirect is happening)
if (currentUser) return null;

return (
<div className="w-full max-w-md rounded-2xl bg-[#141022]/80 backdrop-blur border border-white/10 p-8 shadow-2xl">
<h1 className="text-2xl font-bold text-white text-center">
{mode === 'signup' ? 'Create an Account' : 'Welcome Back'}
{showResetPassword
? 'Reset Password'
: mode === 'signup' ? 'Create an Account' : 'Welcome Back'}
</h1>
<p className="text-slate-400 text-center mt-2">
{mode === 'signup'
? 'Get started designing system architectures for free.'
: 'Sign in to start designing system architectures.'}
{showResetPassword
? 'Enter your email to receive a reset link.'
: mode === 'signup'
? 'Get started designing system architectures for free.'
: 'Sign in to start designing system architectures.'}
</p>

{/* Error message */}
Expand All @@ -90,45 +184,164 @@ export default function AuthCard({ mode = 'login' }: AuthCardProps) {
</div>
)}

{/* Google */}
<button
onClick={() => handleProviderSignIn(signInWithGoogle, setIsLoadingGoogle, 'google')}
disabled={isLoading}
aria-busy={isLoadingGoogle}
className="mt-6 w-full flex items-center justify-center gap-3 rounded-lg bg-[#1f1b33] hover:bg-[#2a2450] border border-white/10 py-3 text-white font-medium transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoadingGoogle ? (
<>
<Spinner label="Signing in" />
Signing in...
</>
) : (
<>
<Image src="/google.svg" width={20} height={20} alt="" />
Continue with Google
</>
)}
</button>

{/* GitHub */}
<button
onClick={() => handleProviderSignIn(signInWithGitHub, setIsLoadingGitHub, 'github')}
disabled={isLoading}
aria-busy={isLoadingGitHub}
className="mt-3 w-full flex items-center justify-center gap-3 rounded-lg bg-[#1f1b33] hover:bg-[#2a2450] border border-white/10 py-3 text-white font-medium transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoadingGitHub ? (
<>
<Spinner label="Signing in" />
Signing in...
</>
) : (
<>
<Image src="/github.svg" width={20} height={20} alt="" />
Continue with GitHub
</>
)}
</button>
{/* Success message */}
{successMessage && (
<div className="mt-4 p-3 rounded-lg bg-green-500/10 border border-green-500/20 text-green-400 text-sm text-center">
{successMessage}
</div>
)}

{showResetPassword ? (
/* Password Reset Form */
<form onSubmit={handlePasswordReset} className="mt-6 space-y-4">
<div>
<label htmlFor="reset-email" className="block text-sm font-medium text-slate-300 mb-1.5">Email</label>
<input
id="reset-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
required
className="w-full rounded-lg bg-[#1f1b33] border border-white/10 px-4 py-3 text-white placeholder-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition"
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full flex items-center justify-center gap-2 rounded-lg bg-primary hover:bg-primary-hover py-3 text-white font-semibold transition disabled:opacity-50 disabled:cursor-not-allowed"
>
Send Reset Link
</button>
<button
type="button"
onClick={() => { setShowResetPassword(false); setSignInError(null); }}
className="w-full text-sm text-slate-400 hover:text-white transition-colors"
>
← Back to sign in
</button>
</form>
) : (
<>
{/* Email/Password Form */}
<form onSubmit={handleEmailSubmit} className="mt-6 space-y-4">
{mode === 'signup' && (
<div>
<label htmlFor="auth-name" className="block text-sm font-medium text-slate-300 mb-1.5">Full Name</label>
<input
id="auth-name"
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="John Doe"
required
disabled={isLoading}
className="w-full rounded-lg bg-[#1f1b33] border border-white/10 px-4 py-3 text-white placeholder-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition disabled:opacity-50"
/>
</div>
)}
<div>
<label htmlFor="auth-email" className="block text-sm font-medium text-slate-300 mb-1.5">Email</label>
<input
id="auth-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
required
disabled={isLoading}
className="w-full rounded-lg bg-[#1f1b33] border border-white/10 px-4 py-3 text-white placeholder-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition disabled:opacity-50"
/>
</div>
<div>
<div className="flex items-center justify-between mb-1.5">
<label htmlFor="auth-password" className="block text-sm font-medium text-slate-300">Password</label>
{mode === 'login' && (
<button
type="button"
onClick={() => { setShowResetPassword(true); setSignInError(null); setSuccessMessage(null); }}
className="text-xs text-primary hover:underline"
>
Forgot password?
</button>
)}
</div>
<input
id="auth-password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={mode === 'signup' ? 'At least 6 characters' : '••••••••'}
required
minLength={6}
disabled={isLoading}
className="w-full rounded-lg bg-[#1f1b33] border border-white/10 px-4 py-3 text-white placeholder-slate-500 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition disabled:opacity-50"
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full flex items-center justify-center gap-2 rounded-lg bg-primary hover:bg-primary-hover py-3 text-white font-semibold transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoadingEmail ? (
<>
<Spinner label="Processing" />
{mode === 'signup' ? 'Creating account...' : 'Signing in...'}
</>
) : (
mode === 'signup' ? 'Create Account' : 'Sign In'
)}
</button>
</form>

{/* Divider */}
<div className="mt-5 flex items-center gap-4">
<div className="flex-1 h-px bg-white/10" />
<span className="text-xs text-slate-500 uppercase tracking-wider">or continue with</span>
<div className="flex-1 h-px bg-white/10" />
</div>

{/* Google */}
<button
onClick={() => handleProviderSignIn(signInWithGoogle, setIsLoadingGoogle, 'google')}
disabled={isLoading}
aria-busy={isLoadingGoogle}
className="mt-4 w-full flex items-center justify-center gap-3 rounded-lg bg-[#1f1b33] hover:bg-[#2a2450] border border-white/10 py-3 text-white font-medium transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoadingGoogle ? (
<>
<Spinner label="Signing in" />
Signing in...
</>
) : (
<>
<Image src="/google.svg" width={20} height={20} alt="" />
Google
</>
)}
</button>

{/* GitHub */}
<button
onClick={() => handleProviderSignIn(signInWithGitHub, setIsLoadingGitHub, 'github')}
disabled={isLoading}
aria-busy={isLoadingGitHub}
className="mt-3 w-full flex items-center justify-center gap-3 rounded-lg bg-[#1f1b33] hover:bg-[#2a2450] border border-white/10 py-3 text-white font-medium transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoadingGitHub ? (
<>
<Spinner label="Signing in" />
Signing in...
</>
) : (
<>
<Image src="/github.svg" width={20} height={20} alt="" />
GitHub
</>
)}
</button>
</>
)}

{/* Footer text */}
<p className="mt-6 text-center text-sm text-slate-500">
Expand Down
6 changes: 4 additions & 2 deletions components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { useAuth } from "../src/lib/firebase/AuthContext";
import { logout } from "../src/lib/firebase/auth";

export default function Navbar() {
const { user, isLoading } = useAuth();

const pathname = usePathname();
const isAuthPage = pathname === '/login' || pathname === '/signup';
return (
<header className="fixed top-0 left-0 right-0 z-50 border-b border-gray-200 dark:border-white/10 bg-background-light/80 dark:bg-background-dark/80 backdrop-blur-md">
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
Expand Down Expand Up @@ -48,7 +50,7 @@ export default function Navbar() {
<div className="hidden sm:block w-16 h-5 bg-white/10 rounded animate-pulse" />
<div className="w-24 h-9 bg-white/10 rounded-lg animate-pulse" />
</div>
) : !user ? (
) : isAuthPage ? null : !user ? (
<>
<Link
href="/login"
Expand Down
Loading
Loading