feat: 3-click SSO registration - unified /auth page, Google/Apple/GitHub OAuth, email fallback, provider model
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
"use client";
|
||||
|
||||
import { useState, FormEvent } from "react";
|
||||
import { useAuth } from "@/lib/AuthContext";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Mail, ArrowLeft } from "lucide-react";
|
||||
|
||||
type AuthMode = "login" | "register";
|
||||
type PageState = "select" | "email-form";
|
||||
|
||||
/** Inline Google SVG logo */
|
||||
function GoogleLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
fill="#EA4335"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Inline Apple SVG logo */
|
||||
function AppleLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Inline GitHub SVG logo */
|
||||
function GitHubLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AuthPage() {
|
||||
const { login, register, oauthLogin, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const [pageState, setPageState] = useState<PageState>("select");
|
||||
const [mode, setMode] = useState<AuthMode>("register");
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleProviderClick = (provider: string) => {
|
||||
setError("");
|
||||
oauthLogin(provider);
|
||||
};
|
||||
|
||||
const handleEmailClick = () => {
|
||||
setError("");
|
||||
setPageState("email-form");
|
||||
};
|
||||
|
||||
const toggleMode = () => {
|
||||
setMode((m) => (m === "login" ? "register" : "login"));
|
||||
setError("");
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
|
||||
if (!email.trim() || !password.trim()) {
|
||||
setError("Please fill in all fields");
|
||||
return;
|
||||
}
|
||||
if (mode === "register" && !name.trim()) {
|
||||
setError("Please enter your name");
|
||||
return;
|
||||
}
|
||||
if (mode === "register" && password.length < 6) {
|
||||
setError("Password must be at least 6 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
if (mode === "login") {
|
||||
await login(email.trim(), password);
|
||||
} else {
|
||||
await register(email.trim(), name.trim(), password);
|
||||
}
|
||||
router.push("/");
|
||||
} catch (err: unknown) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : "Something went wrong. Please try again.";
|
||||
setError(message);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Select State ──────────────────────────────────────────────────────────
|
||||
if (pageState === "select") {
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] flex flex-col items-center justify-center px-5">
|
||||
<div className="w-full max-w-sm">
|
||||
{/* Logo / Brand */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-[#D4AF37]/20 to-[#D4AF37]/5 border border-[#D4AF37]/30 flex items-center justify-center mx-auto mb-4">
|
||||
<span className="text-3xl">☪️</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white">Welcome to Falah</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Your Islamic lifestyle companion
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* SSO Buttons */}
|
||||
<div className="space-y-3">
|
||||
{/* Google */}
|
||||
<button
|
||||
onClick={() => handleProviderClick("google")}
|
||||
disabled={authLoading}
|
||||
className="w-full flex items-center gap-4 px-5 py-4 rounded-2xl bg-[#111118] border border-gray-800/60 active:bg-[#1a1a24] disabled:opacity-50 transition-all touch-manipulation"
|
||||
>
|
||||
<GoogleLogo className="w-6 h-6 shrink-0" />
|
||||
<span className="text-sm font-medium text-white">
|
||||
Continue with Google
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Apple */}
|
||||
<button
|
||||
onClick={() => handleProviderClick("apple")}
|
||||
disabled={authLoading}
|
||||
className="w-full flex items-center gap-4 px-5 py-4 rounded-2xl bg-[#111118] border border-gray-800/60 active:bg-[#1a1a24] disabled:opacity-50 transition-all touch-manipulation"
|
||||
>
|
||||
<AppleLogo className="w-6 h-6 shrink-0 text-white" />
|
||||
<span className="text-sm font-medium text-white">
|
||||
Continue with Apple
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* GitHub */}
|
||||
<button
|
||||
onClick={() => handleProviderClick("github")}
|
||||
disabled={authLoading}
|
||||
className="w-full flex items-center gap-4 px-5 py-4 rounded-2xl bg-[#111118] border border-gray-800/60 active:bg-[#1a1a24] disabled:opacity-50 transition-all touch-manipulation"
|
||||
>
|
||||
<GitHubLogo className="w-6 h-6 shrink-0 text-white" />
|
||||
<span className="text-sm font-medium text-white">
|
||||
Continue with GitHub
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<div className="flex-1 h-px bg-gray-800/60" />
|
||||
<span className="text-xs text-gray-600">or</span>
|
||||
<div className="flex-1 h-px bg-gray-800/60" />
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<button
|
||||
onClick={handleEmailClick}
|
||||
disabled={authLoading}
|
||||
className="w-full flex items-center gap-4 px-5 py-4 rounded-2xl bg-[#D4AF37]/10 border border-[#D4AF37]/25 active:bg-[#D4AF37]/20 disabled:opacity-50 transition-all touch-manipulation"
|
||||
>
|
||||
<div className="w-6 h-6 shrink-0 flex items-center justify-center">
|
||||
<Mail size={22} className="text-[#D4AF37]" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-[#D4AF37]">
|
||||
Continue with Email
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Demo Credentials */}
|
||||
<div className="mt-8 rounded-2xl bg-[#111118] border border-gray-800/60 p-4 text-center">
|
||||
<p className="text-xs text-gray-600 uppercase tracking-wider mb-1.5">
|
||||
Demo Account
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Email:{" "}
|
||||
<span className="text-gray-300 font-mono">demo@falahos.my</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Password:{" "}
|
||||
<span className="text-gray-300 font-mono">password123</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Email Form State ──────────────────────────────────────────────────────
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] flex flex-col items-center justify-center px-5">
|
||||
<div className="w-full max-w-sm">
|
||||
{/* Back button */}
|
||||
<button
|
||||
onClick={() => setPageState("select")}
|
||||
className="flex items-center gap-1.5 text-xs text-gray-500 mb-6 active:text-gray-300 transition-colors touch-manipulation"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
All sign-in methods
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center mb-7">
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#D4AF37]/20 to-[#D4AF37]/5 border border-[#D4AF37]/30 flex items-center justify-center mx-auto mb-3">
|
||||
<Mail size={22} className="text-[#D4AF37]" />
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-white">
|
||||
{mode === "login" ? "Sign In" : "Create Account"}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{mode === "login"
|
||||
? "Welcome back! Sign in to continue"
|
||||
: "Start your Islamic lifestyle journey"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="mb-4 p-3 rounded-xl bg-red-900/20 border border-red-800/40 text-red-400 text-sm text-center">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Name field (register only) */}
|
||||
{mode === "register" && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="auth-name"
|
||||
className="block text-xs font-medium text-gray-500 mb-1.5 uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="auth-name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Your name"
|
||||
autoComplete="name"
|
||||
className="w-full bg-[#111118] border border-gray-800/60 rounded-xl px-4 py-3.5 text-white placeholder-gray-600 focus:outline-none focus:border-[#D4AF37]/50 focus:ring-1 focus:ring-[#D4AF37]/30 transition text-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="auth-email"
|
||||
className="block text-xs font-medium text-gray-500 mb-1.5 uppercase tracking-wider"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="auth-email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@example.com"
|
||||
autoComplete="email"
|
||||
autoFocus
|
||||
className="w-full bg-[#111118] border border-gray-800/60 rounded-xl px-4 py-3.5 text-white placeholder-gray-600 focus:outline-none focus:border-[#D4AF37]/50 focus:ring-1 focus:ring-[#D4AF37]/30 transition text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="auth-password"
|
||||
className="block text-xs font-medium text-gray-500 mb-1.5 uppercase tracking-wider"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="auth-password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={mode === "register" ? "At least 6 characters" : "••••••••"}
|
||||
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
||||
className="w-full bg-[#111118] border border-gray-800/60 rounded-xl px-4 py-3.5 text-white placeholder-gray-600 focus:outline-none focus:border-[#D4AF37]/50 focus:ring-1 focus:ring-[#D4AF37]/30 transition text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="w-full py-3.5 rounded-xl bg-[#D4AF37] text-[#0a0a0f] font-semibold text-sm active:opacity-80 disabled:opacity-50 transition touch-manipulation"
|
||||
>
|
||||
{submitting ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<span className="w-4 h-4 rounded-full border-2 border-[#0a0a0f]/30 border-t-[#0a0a0f] animate-spin" />
|
||||
{mode === "login" ? "Signing in..." : "Creating account..."}
|
||||
</span>
|
||||
) : mode === "login" ? (
|
||||
"Sign In"
|
||||
) : (
|
||||
"Create Account"
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Toggle mode */}
|
||||
<div className="text-center mt-6">
|
||||
<p className="text-xs text-gray-600">
|
||||
{mode === "login" ? (
|
||||
<>
|
||||
Don't have an account?{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleMode}
|
||||
className="text-[#D4AF37] font-medium hover:underline touch-manipulation"
|
||||
>
|
||||
Create one
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Already have an account?{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleMode}
|
||||
className="text-[#D4AF37] font-medium hover:underline touch-manipulation"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* New Member Bonus (register only) */}
|
||||
{mode === "register" && (
|
||||
<div className="mt-6 rounded-xl bg-[#111118] border border-gray-800/60 p-3 text-center">
|
||||
<p className="text-xs text-gray-600 uppercase tracking-wider mb-1">
|
||||
New Member Bonus
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Get <span className="text-[#D4AF37] font-semibold">5,000 FLH</span>{" "}
|
||||
free on sign up + 7-day premium trial
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Demo credentials (subtle) */}
|
||||
<div className="mt-6 rounded-xl bg-[#111118]/60 border border-gray-800/40 p-3 text-center">
|
||||
<p className="text-xs text-gray-600">
|
||||
Demo:{" "}
|
||||
<span className="text-gray-500 font-mono">demo@falahos.my</span>{" "}
|
||||
<span className="text-gray-600">/</span>{" "}
|
||||
<span className="text-gray-500 font-mono">password123</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user