357 lines
10 KiB
TypeScript
357 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { signIn } from "next-auth/react";
|
|
|
|
/**
|
|
* Vibn sign-in / sign-up screen.
|
|
*
|
|
* Ported from design-templates/VIBN (2) (auth.css + signin/signup.jsx). Email +
|
|
* password is the primary action (custom endpoints at /api/auth/register and
|
|
* /api/auth/login that create real NextAuth database sessions); Google OAuth is
|
|
* offered below. Mode is driven by the `?new=1` query param.
|
|
*/
|
|
export default function AuthScreen({
|
|
mode = "signin",
|
|
}: {
|
|
mode?: "signin" | "signup";
|
|
}) {
|
|
const isSignup = mode === "signup";
|
|
|
|
const [name, setName] = useState("");
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [googleLoading, setGoogleLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const emailValid = /\S+@\S+\.\S+/.test(email);
|
|
const canSubmit = isSignup
|
|
? emailValid &&
|
|
password.length >= 8 &&
|
|
name.trim().length > 0 &&
|
|
!submitting
|
|
: emailValid && password.length >= 8 && !submitting;
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!canSubmit) return;
|
|
setError(null);
|
|
setSubmitting(true);
|
|
try {
|
|
const endpoint = isSignup ? "/api/auth/register" : "/api/auth/login";
|
|
const res = await fetch(endpoint, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(
|
|
isSignup
|
|
? { email, password, name: name.trim() || undefined }
|
|
: { email, password },
|
|
),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
setError(data.error || "Something went wrong. Please try again.");
|
|
setSubmitting(false);
|
|
return;
|
|
}
|
|
// Full reload so the session cookie is picked up and AuthFlow routes the
|
|
// user onward (new account -> /onboarding, returning -> dashboard).
|
|
window.location.href = "/signin";
|
|
} catch {
|
|
setError("Network error. Please try again.");
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleGoogle = () => {
|
|
if (googleLoading) return;
|
|
setGoogleLoading(true);
|
|
const cb = typeof window !== "undefined" ? window.location.href : "/signin";
|
|
signIn("google", { callbackUrl: cb });
|
|
};
|
|
|
|
return (
|
|
<div className="vibn-auth">
|
|
<div className="page">
|
|
<header className="topbar">
|
|
<Link href="/" className="logo" aria-label="Vibn home">
|
|
<span className="logo-mark">
|
|
<svg
|
|
viewBox="0 0 36 32"
|
|
width="74%"
|
|
height="74%"
|
|
fill="currentColor"
|
|
stroke="currentColor"
|
|
strokeWidth={1.2}
|
|
strokeLinejoin="round"
|
|
aria-hidden="true"
|
|
>
|
|
<path d="M4 5 L10 5 L12 18 L14 5 L20 5 L12 27 Z" />
|
|
<rect
|
|
x="22.5"
|
|
y="23"
|
|
width="9.5"
|
|
height="3.8"
|
|
rx="0.7"
|
|
className="logo-caret"
|
|
/>
|
|
</svg>
|
|
</span>
|
|
<span>vibn</span>
|
|
</Link>
|
|
<Link href="/" className="topbar-back">
|
|
<svg
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
d="M13 8H3M7 4 3 8l4 4"
|
|
stroke="currentColor"
|
|
strokeWidth={1.6}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
Back to home
|
|
</Link>
|
|
</header>
|
|
|
|
<main className="auth-main">
|
|
<Glows />
|
|
|
|
<div className="auth-card">
|
|
<div className="auth-eye">
|
|
{isSignup ? "Get started" : "Welcome back"}
|
|
</div>
|
|
<h1 className="auth-title">
|
|
{isSignup ? (
|
|
<>
|
|
Create your <em>account</em>.
|
|
</>
|
|
) : (
|
|
<>
|
|
Sign in and <em>keep building</em>.
|
|
</>
|
|
)}
|
|
</h1>
|
|
<p className="auth-sub">
|
|
{isSignup ? null : "Pick up right where you left off."}
|
|
</p>
|
|
|
|
<form className="auth-form" onSubmit={handleSubmit} noValidate>
|
|
{isSignup && (
|
|
<div className="auth-field">
|
|
<label className="auth-label" htmlFor="name">
|
|
Name
|
|
</label>
|
|
<input
|
|
id="name"
|
|
type="text"
|
|
autoComplete="name"
|
|
required
|
|
className="auth-input"
|
|
placeholder="First name or handle"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="auth-field">
|
|
<label className="auth-label" htmlFor="email">
|
|
Email
|
|
</label>
|
|
<input
|
|
id="email"
|
|
type="email"
|
|
autoComplete="email"
|
|
required
|
|
autoFocus={!isSignup}
|
|
className="auth-input"
|
|
placeholder="you@somewhere.com"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="auth-field">
|
|
<label className="auth-label" htmlFor="password">
|
|
Password
|
|
</label>
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
autoComplete={isSignup ? "new-password" : "current-password"}
|
|
required
|
|
minLength={8}
|
|
className="auth-input"
|
|
placeholder={
|
|
isSignup ? "At least 8 characters" : "Your password"
|
|
}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{error && <div className="auth-error">{error}</div>}
|
|
|
|
<button
|
|
type="submit"
|
|
className="auth-btn auth-btn-primary"
|
|
disabled={!canSubmit}
|
|
style={{ marginTop: 4 }}
|
|
>
|
|
{submitting ? (
|
|
<>
|
|
<span className="auth-spinner" />{" "}
|
|
{isSignup ? "Creating your account…" : "Signing in…"}
|
|
</>
|
|
) : isSignup ? (
|
|
<>
|
|
Create my account <Arrow size={13} />
|
|
</>
|
|
) : (
|
|
<>Sign in</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
|
|
<div className="auth-divider">or continue with</div>
|
|
|
|
<div className="auth-oauth">
|
|
<button
|
|
type="button"
|
|
className="auth-btn auth-btn-ghost"
|
|
onClick={handleGoogle}
|
|
disabled={googleLoading}
|
|
>
|
|
{googleLoading ? (
|
|
<>
|
|
<span className="auth-spinner ghost" /> Connecting…
|
|
</>
|
|
) : (
|
|
<>
|
|
<GoogleIcon />{" "}
|
|
{isSignup ? "Sign up with Google" : "Continue with Google"}
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{isSignup && (
|
|
<p className="auth-fine">
|
|
By continuing you agree to our{" "}
|
|
<Link href="/mission">Terms</Link> and{" "}
|
|
<Link href="/mission">Privacy Policy</Link>.
|
|
</p>
|
|
)}
|
|
|
|
<div className="auth-foot">
|
|
{isSignup ? (
|
|
<>
|
|
Already have an account? <Link href="/signin">Sign in →</Link>
|
|
</>
|
|
) : (
|
|
<>
|
|
New to Vibn? <Link href="/signup">Create an account →</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="auth-built-canada">🇨🇦 Built in Canada</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Glows() {
|
|
return (
|
|
<>
|
|
<div
|
|
className="auth-glow"
|
|
style={{
|
|
width: 700,
|
|
height: 700,
|
|
top: -150,
|
|
left: "50%",
|
|
transform: "translateX(-50%)",
|
|
background:
|
|
"radial-gradient(circle at center, oklch(0.74 0.175 35 / 0.22) 0%, transparent 62%)",
|
|
}}
|
|
/>
|
|
<div
|
|
className="auth-glow"
|
|
style={{
|
|
width: 500,
|
|
height: 500,
|
|
bottom: -100,
|
|
left: 0,
|
|
background:
|
|
"radial-gradient(circle at center, oklch(0.45 0.10 35 / 0.20) 0%, transparent 62%)",
|
|
}}
|
|
/>
|
|
<div
|
|
className="auth-glow"
|
|
style={{
|
|
width: 450,
|
|
height: 450,
|
|
top: "50%",
|
|
right: -150,
|
|
background:
|
|
"radial-gradient(circle at center, oklch(0.45 0.10 35 / 0.15) 0%, transparent 62%)",
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function Arrow({ size = 14 }: { size?: number }) {
|
|
return (
|
|
<svg
|
|
width={size}
|
|
height={size}
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
d="M3 8h10M9 4l4 4-4 4"
|
|
stroke="currentColor"
|
|
strokeWidth={1.6}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function GoogleIcon({ size = 17 }: { size?: number }) {
|
|
return (
|
|
<svg width={size} height={size} viewBox="0 0 18 18" aria-hidden="true">
|
|
<path
|
|
fill="#EA4335"
|
|
d="M9 3.6c1.3 0 2.5.5 3.4 1.3l2.5-2.5C13.4 1 11.3.1 9 .1 5.5.1 2.4 2.1.9 5.1l2.9 2.3C4.5 5.2 6.6 3.6 9 3.6Z"
|
|
/>
|
|
<path
|
|
fill="#34A853"
|
|
d="M17.6 9.2c0-.6-.1-1.2-.2-1.8H9v3.4h4.9c-.2 1.1-.9 2-1.9 2.6l2.9 2.3c1.7-1.6 2.7-3.9 2.7-6.5Z"
|
|
/>
|
|
<path
|
|
fill="#FBBC05"
|
|
d="M3.8 10.7c-.2-.6-.3-1.1-.3-1.7s.1-1.2.3-1.7L.9 5C.3 6.2 0 7.5 0 9s.3 2.8.9 4l2.9-2.3Z"
|
|
/>
|
|
<path
|
|
fill="#4285F4"
|
|
d="M9 17.9c2.4 0 4.4-.8 5.9-2.2l-2.9-2.3c-.8.5-1.8.9-3 .9-2.3 0-4.3-1.6-5-3.7L1.1 12.9C2.6 15.9 5.6 17.9 9 17.9Z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|