2025-09-22 11:14:11 +08:00

295 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useCallback, useEffect } from "react";
import "./style/login.css";
import { useRouter, useSearchParams } from "next/navigation";
import React from "react";
import Link from "next/link";
import { signInWithGoogle, loginUser } from "@/lib/auth";
import { GradientText } from "@/components/ui/gradient-text";
import { GoogleLoginButton } from "@/components/ui/google-login-button";
import { Eye, EyeOff } from "lucide-react";
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formError, setFormError] = useState("");
const [successMessage, setSuccessMessage] = useState("");
const [passwordError, setPasswordError] = useState("");
const [googleLoading, setGoogleLoading] = useState(false);
const [emailFocused, setEmailFocused] = useState(false);
const [passwordFocused, setPasswordFocused] = useState(false);
const router = useRouter();
const searchParams = useSearchParams();
/** 密码验证函数 */
/**
* Password validation function with English prompts
* @param {string} password - The password to validate
* @returns {string} - Error message if invalid, empty string if valid
*/
const validatePassword = (password: string): string => {
if (password.length < 8) {
return "Password must be at least 8 characters";
}
if (password.length > 18) {
return "Password cannot exceed 18 characters";
}
if (!/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^*&\.]{8,18}$/.test(password)) {
return "Password must contain both letters and numbers";
}
return "";
};
/** 处理密码输入变化 */
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newPassword = e.target.value;
setPassword(newPassword);
if (newPassword) {
const error = validatePassword(newPassword);
setPasswordError(error);
} else {
setPasswordError("");
}
};
// Check for registered=true parameter
useEffect(() => {
const registered = searchParams?.get("registered");
if (registered === "true") {
setSuccessMessage(
"Registration successful! Please login with your new account."
);
}
const error = searchParams?.get("error");
if (error) {
if (error === "google_oauth") {
setFormError("Google login failed, please try again.");
} else if (error === "auth_failed") {
setFormError("Authentication failed, please try again.");
} else if (error === "oauth_callback_incomplete") {
setFormError("OAuth callback processing is incomplete. Please use the Google login button below.");
} else if (error === "deprecated_oauth_callback") {
setFormError("The old OAuth method is deprecated. Please use the Google login button below for a better experience.");
}
}
}, [searchParams]);
const handleGoogleSignIn = async () => {
try {
setGoogleLoading(true);
setFormError("");
// 获取邀请码从URL参数或其他来源
const inviteCode = searchParams?.get("invite") || undefined;
// 使用Google GSI SDK进行登录
await signInWithGoogle(inviteCode);
} catch (error: any) {
console.error("Google sign-in error:", error);
setFormError(error.message || "Google sign-in failed, please try again");
setGoogleLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 验证密码
const passwordValidationError = validatePassword(password);
if (passwordValidationError) {
setPasswordError(passwordValidationError);
return;
}
setIsSubmitting(true);
setFormError("");
setSuccessMessage("");
try {
await loginUser(email, password);
// 登录成功后跳转到首页
router.push("/movies");
} catch (error: any) {
console.error("Login failed:", error);
// 根据错误类型显示不同的错误消息
setFormError(
error.message || "Login failed, please try again."
);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="min-h-screen relative overflow-hidden">
{/* 背景视频 */}
<video
autoPlay
loop
muted
playsInline
className="absolute inset-0 w-full h-full object-cover"
data-alt="background-video"
>
<source src="/assets/login.mp4" type="video/mp4" />
{/* 如果没有视频文件,显示渐变背景 */}
</video>
{/* 视频遮罩层 */}
<div
className="absolute inset-0 bg-black/20 "
data-alt="video-overlay"
></div>
{/* Logo */}
<div
className="absolute top-8 left-8 z-50"
data-alt="logo-container"
onClick={() => router.push("/")}
>
<span className="logo-heart cursor-pointer">
<GradientText
text="MovieFlow"
startPercentage={30}
endPercentage={70}
/>
</span>
{/* beta标签 */}
<span className="inline-flex items-center px-1.5 py-0.5 text-[8px] font-semibold tracking-wider text-[rgb(212 202 202)] border border-[rgba(106,244,249,0.2)] rounded-full shadow-[0_0_10px_rgba(106,244,249,0.1)]">
Beta
</span>
</div>
{/* 登录框 - 居中显示 */}
<div className="relative bottom-4 z-10 flex items-center justify-center min-h-screen p-4">
<div className="auth-container max-w-md w-full !bg-black/40 backdrop-blur-lg border border-white/20 rounded-2xl p-6 shadow-2xl">
<div className="auth-header text-center mb-4">
<h2 className="text-2xl font-bold text-white pb-2">Login</h2>
<p className="text-gray-300">Your inspiration is waiting. <br />Log in to bring your vision to life.</p>
</div>
<form onSubmit={handleSubmit}>
{successMessage && (
<div className="bg-green-500/20 text-green-300 p-3 mb-4 rounded-lg border border-green-500/20">
{successMessage}
</div>
)}
<div className="mb-6 relative">
<label
className={`absolute left-3 transition-all duration-200 pointer-events-none ${
email || emailFocused
? 'top-0 -translate-y-1/2 text-base text-white bg-[#141400]/30 px-2 rounded'
: 'opacity-0'
}`}
>
Email
</label>
<input
placeholder="Email"
required
className="form-control"
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
onFocus={() => setEmailFocused(true)}
onBlur={() => setEmailFocused(false)}
/>
</div>
<div className="mb-4">
<div className="relative">
<label
className={`absolute left-3 transition-all duration-200 pointer-events-none z-10 ${
password || passwordFocused
? 'top-0 -translate-y-1/2 text-base text-white bg-[#141400]/30 px-2 rounded'
: 'opacity-0'
}`}
>
Password
</label>
<input
placeholder="Password (8-18 characters, letters, numbers and !@#$%^*&.)"
required
className={`form-control pr-10 ${
passwordError ? "border-red-500/50" : ""
}`}
type={showPassword ? "text" : "password"}
value={password}
onChange={handlePasswordChange}
onFocus={() => setPasswordFocused(true)}
onBlur={() => setPasswordFocused(false)}
/>
<button
type="button"
data-alt="toggle-password-visibility"
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-300 focus:outline-none"
onClick={() => setShowPassword(!showPassword)}
>
{!showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
</button>
</div>
{passwordError && (
<p className="mt-4 text-sm text-red-400">{passwordError}</p>
)}
{/* <div className="flex justify-end mt-2">
<a
className="auth-link small"
href="/forgot-password"
data-discover="true"
>
Forgot password?
</a>
</div> */}
</div>
{formError && (
<div className="bg-red-500/20 text-red-300 p-3 mb-6 rounded-lg border border-red-500/20">
{formError}
</div>
)}
<button
type="submit"
className="w-full mt-4 btn py-3 rounded-lg cursor-pointer !bg-[#C039F6] hover:!bg-[#C039F6]/80"
disabled={isSubmitting || !!passwordError || !password}
>
{isSubmitting ? "Logging in..." : "Login"}
</button>
{/* 谷歌登录按钮已暂时注释 */}
<div className="my-4 relative flex items-center">
<div className="flex-grow border-t border-gray-500/30"></div>
<span className="flex-shrink mx-4 text-gray-400">or</span>
<div className="flex-grow border-t border-gray-500/30"></div>
</div>
<GoogleLoginButton
onClick={handleGoogleSignIn}
loading={googleLoading}
disabled={isSubmitting}
variant="outline"
size="md"
className="w-full"
/>
<div className="text-center mt-4">
<p style={{ color: "rgba(255, 255, 255, 0.6)" }}>
Don't have an account?{" "}
<Link href="/signup" className="auth-link !text-[#C039F6]">
Sign up
</Link>
</p>
</div>
</form>
</div>
</div>
</div>
);
}