js • Lines: 234import React, { useState, useEffect, useCallback } from "react";
import { useAuth } from "../contexts/AuthContext";
import { useNavigate, useSearchParams } from "react-router-dom";
import { doc, getDoc, setDoc } from "firebase/firestore";
import { db } from "../firebase";
import { Container, Card, Button, Form, Alert, Spinner, InputGroup } from "react-bootstrap";
import { FcGoogle } from "react-icons/fc";
import { FaEnvelope } from "react-icons/fa";
import { col } from "../utils/firestorePath";
const friendlyError = (code) => {
const map = {
"auth/user-not-found": "No account found with this email.",
"auth/wrong-password": "Incorrect password. Try again.",
"auth/invalid-credential": "Invalid email or password.",
"auth/email-already-in-use": "An account with this email already exists.",
"auth/weak-password": "Password must be at least 6 characters.",
"auth/invalid-email": "Please enter a valid email address.",
"auth/too-many-requests": "Too many attempts. Please wait a moment and try again.",
"auth/popup-closed-by-user": "Sign-in popup was closed before completing.",
"auth/popup-blocked": "Popup was blocked by your browser. Allow popups for this site.",
"auth/cancelled-popup-request": "Sign-in cancelled.",
"auth/account-exists-with-different-credential": "An account already exists with this email using a different sign-in method.",
"auth/requires-recent-login": "Please log out and log back in to perform this action.",
"auth/network-request-failed": "Network error. Check your internet connection.",
"auth/operation-not-allowed": "This sign-in method is not enabled. Contact the site owner.",
"auth/user-disabled": "This account has been disabled.",
"auth/unauthorized-domain": "This domain is not authorized for sign-in.",
};
const key = code?.startsWith("auth/") ? code : `auth/${code}`;
const msg = map[key] || map[code];
return msg || `Something went wrong (${code}). Please try again.`;
};
const USERNAME_REGEX = /^[a-zA-Z0-9_-]+$/;
async function checkUsernameAvailable(username) {
if (!username || username.length < 3) return null;
const snap = await getDoc(doc(db, col("usernames"), username.toLowerCase()));
return !snap.exists();
}
export default function Login() {
const { signInWithGoogle, signUpWithEmail, signInWithEmail, refreshProfile } = useAuth();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const redirectBase = searchParams.get("redirect") || "/contents/scores";
const likeParam = searchParams.get("like");
const redirectTo = redirectBase + (likeParam ? "?like=1" : "");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [username, setUsername] = useState("");
const [usernameAvail, setUsernameAvail] = useState(null);
const [checkingUser, setCheckingUser] = useState(false);
const [isSignUp, setIsSignUp] = useState(false);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [verificationSent, setVerificationSent] = useState(false);
const checkAvail = useCallback(async (val) => {
if (!val || val.length < 3) { setUsernameAvail(null); return; }
setCheckingUser(true);
setUsernameAvail(await checkUsernameAvailable(val));
setCheckingUser(false);
}, []);
useEffect(() => {
const t = setTimeout(() => checkAvail(username), 400);
return () => clearTimeout(t);
}, [username, checkAvail]);
const usernameErr = () => {
if (!username || !isSignUp) return "";
if (username.length < 3) return "At least 3 characters.";
if (username.length > 20) return "At most 20 characters.";
if (!USERNAME_REGEX.test(username)) return "Letters, numbers, underscores, hyphens only.";
if (usernameAvail === false) return "Username taken.";
return "";
};
const redirectAfterLogin = async (uid) => {
for (let i = 0; i < 10; i++) {
const snap = await getDoc(doc(db, col("profiles"), uid));
if (snap.exists()) {
if (!snap.data()?.username) {
navigate("/setup-username");
} else {
navigate(redirectTo);
}
return;
}
await new Promise(r => setTimeout(r, 300));
}
navigate("/");
};
const handleSocialLogin = (fn) => async () => {
try {
setError("");
setLoading(true);
const cred = await fn();
await redirectAfterLogin(cred.user.uid);
} catch (e) {
console.log("Auth error:", e.code, e.message);
setError(friendlyError(e.code));
} finally {
setLoading(false);
}
};
const handleEmailAuth = async (e) => {
e.preventDefault();
setError("");
setVerificationSent(false);
setLoading(true);
try {
if (isSignUp) {
const uErr = usernameErr();
if (uErr || usernameAvail !== true) {
throw new Error(uErr || "Choose a different username.");
}
const cred = await signUpWithEmail(email, password);
await setDoc(doc(db, col("usernames"), username.toLowerCase()), { uid: cred.user.uid });
await setDoc(doc(db, col("profiles"), cred.user.uid), { username: username.toLowerCase() }, { merge: true });
await refreshProfile();
setVerificationSent(true);
} else {
const cred = await signInWithEmail(email, password);
await redirectAfterLogin(cred.user.uid);
}
} catch (e) {
setError(friendlyError(e.code) || e.message);
} finally {
setLoading(false);
}
};
const toggleMode = () => {
setIsSignUp(!isSignUp);
setError("");
setVerificationSent(false);
setUsernameAvail(null);
};
return (
<Container className="d-flex justify-content-center mt-5">
<Card bg="dark" text="light" style={{ width: "420px" }}>
<Card.Body>
<Card.Title className="text-center mb-4">
<FaEnvelope className="me-2" />{isSignUp ? "Sign Up" : "Sign In"}
</Card.Title>
{error && <Alert variant="danger" className="py-2 small">{error}</Alert>}
{verificationSent && (
<Alert variant="info" className="py-2 small">
Verification email sent. Please check your inbox before signing in.
</Alert>
)}
<Form onSubmit={handleEmailAuth}>
{isSignUp && (
<Form.Group className="mb-2">
<InputGroup>
<InputGroup.Text className="bg-black text-secondary border-secondary">@</InputGroup.Text>
<Form.Control
type="text"
placeholder="Username"
value={username}
onChange={(e) => { setUsername(e.target.value); setUsernameAvail(null); }}
required
className="bg-dark text-light border-secondary"
autoComplete="off"
spellCheck={false}
/>
</InputGroup>
{username.length >= 3 && (
<Form.Text className={usernameAvail === true ? "text-success" : usernameAvail === false ? "text-danger" : "text-secondary"}>
{checkingUser ? <Spinner animation="border" size="sm" /> :
usernameAvail === true ? "✓ Available" :
usernameAvail === false ? "✗ Taken" : ""}
</Form.Text>
)}
{usernameErr() && usernameAvail !== false && <Form.Text className="text-danger d-block">{usernameErr()}</Form.Text>}
</Form.Group>
)}
<Form.Group className="mb-2">
<Form.Control
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="bg-dark text-light border-secondary"
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Control
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={6}
className="bg-dark text-light border-secondary"
/>
</Form.Group>
<Button type="submit" variant="primary" className="w-100" disabled={loading}>
{loading ? <Spinner size="sm" /> : isSignUp ? "Sign Up with Email" : "Sign In with Email"}
</Button>
</Form>
<div className="text-center mt-3">
<small className="text-secondary" style={{ fontSize: '11px', lineHeight: 1.4 }}>
We only store your email, username, and avatar for authentication and display purposes. No personal data is shared or sold.
</small>
</div>
<div className="text-center mt-2">
<Button variant="link" size="sm" onClick={toggleMode}>
{isSignUp ? "Already have an account? Sign In" : "Don't have an account? Sign Up"}
</Button>
</div>
<hr className="text-secondary" />
<div className="d-grid gap-2">
<Button variant="outline-light" onClick={handleSocialLogin(signInWithGoogle)} disabled={loading}>
<FcGoogle className="me-2" />Sign in with Google
</Button>
</div>
</Card.Body>
</Card>
</Container>
);
}