forked from 77media/video-flow
718 lines
26 KiB
TypeScript
718 lines
26 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { signInWithGoogle, sendVerificationLink, registerUserWithInvite } from "@/lib/auth";
|
|
import { GradientText } from "@/components/ui/gradient-text";
|
|
import { GoogleLoginButton } from "@/components/ui/google-login-button";
|
|
import { Eye, EyeOff, Mail } from "lucide-react";
|
|
|
|
export default function SignupPage() {
|
|
const [name, setName] = useState("");
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
const [inviteCode, setInviteCode] = useState("");
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [formError, setFormError] = useState("");
|
|
const [passwordError, setPasswordError] = useState("");
|
|
const [confirmPasswordError, setConfirmPasswordError] = useState("");
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const [agreeToTerms, setAgreeToTerms] = useState(true);
|
|
const [showActivationModal, setShowActivationModal] = useState(false);
|
|
const [resendCooldown, setResendCooldown] = useState(60);
|
|
const [resendLoading, setResendLoading] = useState(false);
|
|
const [resendMessage, setResendMessage] = useState("");
|
|
const [resendError, setResendError] = useState("");
|
|
const [googleLoading, setGoogleLoading] = useState(false);
|
|
const router = useRouter();
|
|
|
|
// Handle scroll indicator for small screens
|
|
React.useEffect(() => {
|
|
try {
|
|
const url = new URL(window.location.href);
|
|
const codeFromUrl = url.searchParams.get("inviteCode");
|
|
const codeFromSession = sessionStorage.getItem("inviteCode");
|
|
const code = codeFromUrl || codeFromSession || "";
|
|
if (code) {
|
|
setInviteCode(code);
|
|
sessionStorage.setItem("inviteCode", code);
|
|
}
|
|
} catch (err) {}
|
|
const handleScroll = () => {
|
|
const scrollableElement = document.querySelector('.signup-form > div:nth-child(2)');
|
|
const formElement = document.querySelector('.signup-form');
|
|
if (scrollableElement && formElement) {
|
|
const hasScroll = scrollableElement.scrollHeight > scrollableElement.clientHeight;
|
|
const isScrolledToBottom = scrollableElement.scrollTop + scrollableElement.clientHeight >= scrollableElement.scrollHeight - 10;
|
|
|
|
if (hasScroll && !isScrolledToBottom) {
|
|
formElement.classList.add('has-scroll');
|
|
} else {
|
|
formElement.classList.remove('has-scroll');
|
|
}
|
|
}
|
|
};
|
|
|
|
const scrollableElement = document.querySelector('.signup-form > div:nth-child(2)');
|
|
if (scrollableElement) {
|
|
scrollableElement.addEventListener('scroll', handleScroll);
|
|
// Check initially
|
|
handleScroll();
|
|
|
|
return () => {
|
|
scrollableElement.removeEventListener('scroll', handleScroll);
|
|
};
|
|
}
|
|
}, []);
|
|
|
|
/** Password validation function with English prompts */
|
|
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 "";
|
|
};
|
|
|
|
/** Get quick link to email provider by domain */
|
|
const getEmailProviderLink = (addr: string): string | undefined => {
|
|
const domain = addr.split("@")[1]?.toLowerCase();
|
|
if (!domain) return undefined;
|
|
const map: Record<string, string> = {
|
|
"gmail.com": "https://mail.google.com",
|
|
"outlook.com": "https://outlook.live.com/mail",
|
|
"hotmail.com": "https://outlook.live.com/mail",
|
|
"live.com": "https://outlook.live.com/mail",
|
|
"yahoo.com": "https://mail.yahoo.com",
|
|
"icloud.com": "https://www.icloud.com/mail",
|
|
"qq.com": "https://mail.qq.com",
|
|
"163.com": "https://mail.163.com",
|
|
"126.com": "https://mail.126.com",
|
|
"yeah.net": "https://mail.yeah.net",
|
|
};
|
|
return map[domain];
|
|
};
|
|
|
|
/** Handle resend activation email */
|
|
const handleResend = async () => {
|
|
if (resendCooldown > 0 || resendLoading) return;
|
|
try {
|
|
setResendLoading(true);
|
|
setResendMessage("");
|
|
setResendError("");
|
|
await sendVerificationLink(email);
|
|
setResendMessage("Resent. Please check your email.");
|
|
setResendCooldown(60);
|
|
} catch (err: any) {
|
|
setResendError(err?.message || "Sending failed, please try again later");
|
|
} finally {
|
|
setResendLoading(false);
|
|
}
|
|
};
|
|
|
|
/** Countdown for resend button */
|
|
React.useEffect(() => {
|
|
if (!showActivationModal) return;
|
|
if (resendCooldown <= 0) return;
|
|
const timer = setInterval(() => {
|
|
setResendCooldown((s) => (s > 0 ? s - 1 : 0));
|
|
}, 1000);
|
|
return () => clearInterval(timer);
|
|
}, [showActivationModal, resendCooldown]);
|
|
|
|
/** Handle Terms of Service click */
|
|
const handleTermsClick = () => {
|
|
window.open("/Terms", "_blank");
|
|
};
|
|
|
|
/** Handle Privacy Policy click */
|
|
const handlePrivacyClick = () => {
|
|
window.open("/Privacy", "_blank");
|
|
};
|
|
|
|
/** 处理密码输入变化 */
|
|
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const newPassword = e.target.value;
|
|
setPassword(newPassword);
|
|
|
|
if (newPassword) {
|
|
const error = validatePassword(newPassword);
|
|
setPasswordError(error);
|
|
} else {
|
|
setPasswordError("");
|
|
}
|
|
|
|
// 如果确认密码已输入,重新验证确认密码
|
|
if (confirmPassword) {
|
|
if (newPassword !== confirmPassword) {
|
|
setConfirmPasswordError("Passwords do not match");
|
|
} else {
|
|
setConfirmPasswordError("");
|
|
}
|
|
}
|
|
};
|
|
|
|
/** 处理确认密码输入变化 */
|
|
const handleConfirmPasswordChange = (
|
|
e: React.ChangeEvent<HTMLInputElement>
|
|
) => {
|
|
const newConfirmPassword = e.target.value;
|
|
setConfirmPassword(newConfirmPassword);
|
|
|
|
if (newConfirmPassword) {
|
|
if (password !== newConfirmPassword) {
|
|
setConfirmPasswordError("Passwords do not match");
|
|
} else {
|
|
setConfirmPasswordError("");
|
|
}
|
|
} else {
|
|
setConfirmPasswordError("");
|
|
}
|
|
};
|
|
|
|
const handleGoogleSignIn = async () => {
|
|
try {
|
|
setGoogleLoading(true);
|
|
setFormError("");
|
|
// signInWithGoogle now returns a promise and may throw errors
|
|
await signInWithGoogle();
|
|
} 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;
|
|
}
|
|
|
|
// 验证确认密码
|
|
if (password !== confirmPassword) {
|
|
setConfirmPasswordError("Passwords do not match");
|
|
return;
|
|
}
|
|
|
|
// 验证是否同意条款
|
|
if (!agreeToTerms) {
|
|
setFormError("Please agree to the Terms of Service and Privacy Policy");
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
setFormError("");
|
|
|
|
try {
|
|
// Use new registration API
|
|
const response = await registerUserWithInvite({
|
|
name: name,
|
|
email,
|
|
password,
|
|
invite_code: inviteCode || undefined,
|
|
});
|
|
// Clear inviteCode after successful registration
|
|
try {
|
|
sessionStorage.removeItem("inviteCode");
|
|
} catch {}
|
|
|
|
// Show activation modal instead of redirecting to login
|
|
setShowActivationModal(true);
|
|
setResendCooldown(60);
|
|
} catch (error: any) {
|
|
console.error("Signup error:", error);
|
|
setFormError(error.message || "Registration failed, please try again");
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="min-h-screen relative overflow-hidden">
|
|
{/* Height-responsive container styles */}
|
|
<style jsx>{`
|
|
/* Sticky header/footer layout styles */
|
|
.signup-form {
|
|
border-radius: 1rem;
|
|
}
|
|
|
|
@media (max-height: 800px) {
|
|
.signup-container {
|
|
min-height: 100vh;
|
|
min-height: 100dvh; /* Dynamic viewport height for mobile */
|
|
padding: 1rem 1rem 2rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.signup-form {
|
|
margin-top: 2rem;
|
|
margin-bottom: 2rem;
|
|
max-height: calc(100vh - 4rem);
|
|
max-height: calc(100dvh - 4rem);
|
|
}
|
|
}
|
|
|
|
@media (max-height: 600px) {
|
|
.signup-container {
|
|
padding: 0.5rem 1rem 1rem;
|
|
align-items: flex-start;
|
|
}
|
|
.signup-form {
|
|
margin-top: 1rem;
|
|
margin-bottom: 1rem;
|
|
max-height: calc(100vh - 2rem);
|
|
max-height: calc(100dvh - 2rem);
|
|
}
|
|
.form-spacing {
|
|
gap: 0.75rem;
|
|
}
|
|
.form-field-spacing {
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
}
|
|
|
|
@media (max-height: 480px) {
|
|
.signup-container {
|
|
padding: 0.25rem 0.75rem 0.5rem;
|
|
align-items: flex-start;
|
|
}
|
|
.signup-form {
|
|
margin-top: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
max-height: calc(100vh - 1rem);
|
|
max-height: calc(100dvh - 1rem);
|
|
background: rgba(0, 0, 0, 0.6) !important;
|
|
backdrop-filter: blur(12px) !important;
|
|
border: 1px solid rgba(255, 255, 255, 0.15) !important;
|
|
}
|
|
.form-spacing {
|
|
gap: 0.5rem;
|
|
}
|
|
.form-field-spacing {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.compact-header {
|
|
padding: 1rem 1rem 0.5rem !important;
|
|
}
|
|
.compact-header h2 {
|
|
font-size: 1.25rem;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
.compact-header p {
|
|
font-size: 0.875rem;
|
|
}
|
|
.video-overlay-enhanced {
|
|
background: rgba(0, 0, 0, 0.4) !important;
|
|
}
|
|
}
|
|
|
|
/* Enhanced readability for all small screens */
|
|
@media (max-height: 600px) {
|
|
.signup-form {
|
|
background: rgba(0, 0, 0, 0.5) !important;
|
|
backdrop-filter: blur(16px) !important;
|
|
}
|
|
.video-overlay-enhanced {
|
|
background: rgba(0, 0, 0, 0.3) !important;
|
|
}
|
|
}
|
|
|
|
/* Ultra-compact mode for very small screens */
|
|
@media (max-height: 400px) {
|
|
.signup-form {
|
|
border-radius: 1rem !important;
|
|
}
|
|
.compact-header {
|
|
padding: 0.75rem 1rem 0.5rem !important;
|
|
}
|
|
.compact-header h2 {
|
|
font-size: 1.125rem;
|
|
margin-bottom: 0.125rem;
|
|
}
|
|
.compact-header p {
|
|
font-size: 0.8125rem;
|
|
}
|
|
.form-spacing {
|
|
gap: 0.375rem;
|
|
}
|
|
.form-field-spacing {
|
|
margin-bottom: 0.375rem;
|
|
}
|
|
input, button {
|
|
padding: 0.625rem 0.75rem !important;
|
|
font-size: 0.875rem !important;
|
|
}
|
|
label {
|
|
font-size: 0.8125rem !important;
|
|
margin-bottom: 0.25rem !important;
|
|
}
|
|
}
|
|
|
|
/* Scroll indicator for small screens - now applies to the scrollable middle section */
|
|
@media (max-height: 600px) {
|
|
.signup-form > div:nth-child(2) {
|
|
position: relative;
|
|
}
|
|
.signup-form > div:nth-child(2)::after {
|
|
content: '';
|
|
position: sticky;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 20px;
|
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.4));
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
.signup-form.has-scroll > div:nth-child(2)::after {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
`}</style>
|
|
{/* 背景视频 */}
|
|
<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="video-overlay-enhanced 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="signup-container relative z-10 flex items-center justify-center min-h-screen p-4">
|
|
<div className="signup-form max-w-md w-full bg-black/40 backdrop-blur-lg border border-white/20 rounded-2xl shadow-2xl flex flex-col max-h-[90vh]">
|
|
{/* Fixed Header */}
|
|
<div className="compact-header text-center p-6 pb-4 flex-shrink-0">
|
|
<h2 className="text-2xl font-bold text-white mb-2">
|
|
Sign Up, for free
|
|
</h2>
|
|
<p className="text-gray-300">Create your account to get started</p>
|
|
</div>
|
|
|
|
{/* Scrollable Middle Content */}
|
|
<div className="flex-1 overflow-y-auto px-6">
|
|
<form onSubmit={handleSubmit} className="form-spacing space-y-4">
|
|
<div className="form-field-spacing">
|
|
<label className="block text-sm font-medium text-white mb-1">
|
|
Email
|
|
</label>
|
|
<input
|
|
type="email"
|
|
placeholder="your@email.com"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
required
|
|
className="w-full px-4 py-3 rounded-lg bg-black/30 border border-white/20 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
<div className="form-field-spacing">
|
|
<label className="block text-sm font-medium text-white mb-1">
|
|
Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Your name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
required
|
|
className="w-full px-4 py-3 rounded-lg bg-black/30 border border-white/20 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="form-field-spacing">
|
|
<label className="block text-sm font-medium text-white mb-1">
|
|
Password
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showPassword ? "text" : "password"}
|
|
placeholder="8-18 characters, letters, numbers and !@#$%^*&"
|
|
value={password}
|
|
onChange={handlePasswordChange}
|
|
required
|
|
className={`w-full px-4 py-3 pr-12 rounded-lg bg-black/30 border text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent ${
|
|
passwordError ? "border-red-500/50" : "border-white/20"
|
|
}`}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white transition-colors"
|
|
data-alt="toggle-password-visibility"
|
|
>
|
|
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="form-field-spacing mt-3">
|
|
<label className="block text-sm font-medium text-white mb-1">
|
|
Confirm Password
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showConfirmPassword ? "text" : "password"}
|
|
placeholder="Confirm your password"
|
|
value={confirmPassword}
|
|
onChange={handleConfirmPasswordChange}
|
|
required
|
|
className={`w-full px-4 py-3 pr-12 rounded-lg bg-black/30 border text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent ${
|
|
confirmPasswordError
|
|
? "border-red-500/50"
|
|
: "border-white/20"
|
|
}`}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white transition-colors"
|
|
data-alt="toggle-confirm-password-visibility"
|
|
>
|
|
{showConfirmPassword ? (
|
|
<EyeOff size={20} />
|
|
) : (
|
|
<Eye size={20} />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{passwordError && (
|
|
<p className="mt-1 text-sm text-red-400">{passwordError}</p>
|
|
)}
|
|
{confirmPasswordError && (
|
|
<p className="mt-1 text-sm text-red-400">
|
|
{confirmPasswordError}
|
|
</p>
|
|
)}
|
|
{password && !passwordError && (
|
|
<p className="mt-1 text-sm text-green-400">
|
|
✓ Password format is correct
|
|
</p>
|
|
)}
|
|
{confirmPassword && !confirmPasswordError && (
|
|
<p className="mt-1 text-sm text-green-400">✓ Passwords match</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="form-field-spacing">
|
|
<label className="block text-sm font-medium text-white mb-1">
|
|
Invite Code
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Enter invite code if you have one"
|
|
value={inviteCode}
|
|
onChange={(e) => setInviteCode(e.target.value)}
|
|
className="w-full px-4 py-3 rounded-lg bg-black/30 border border-white/20 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="form-field-spacing flex items-start space-x-3">
|
|
<label
|
|
htmlFor="agreeToTerms"
|
|
className="text-sm text-gray-300 leading-relaxed"
|
|
data-alt="terms-privacy-info"
|
|
>
|
|
By clicking “Sign Up”, you agree to our{' '}
|
|
<button
|
|
type="button"
|
|
onClick={handleTermsClick}
|
|
className="text-purple-400 hover:text-purple-300 underline"
|
|
data-alt="terms-link"
|
|
>
|
|
Terms of Service
|
|
</button>{' '}
|
|
and acknowledge that you have read and understand our{' '}
|
|
<button
|
|
type="button"
|
|
onClick={handlePrivacyClick}
|
|
className="text-purple-400 hover:text-purple-300 underline"
|
|
data-alt="privacy-link"
|
|
>
|
|
Privacy Policy
|
|
</button>
|
|
</label>
|
|
</div>
|
|
|
|
{formError && (
|
|
<div className="bg-red-500/20 text-red-300 p-3 rounded-lg border border-red-500/20">
|
|
{formError}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-4 mt-6">
|
|
<Link
|
|
href="/login"
|
|
className="flex-1 py-3 text-center border border-white/20 rounded-lg text-white hover:bg-white/10 transition-colors"
|
|
>
|
|
Back to login
|
|
</Link>
|
|
<button
|
|
type="submit"
|
|
disabled={
|
|
isSubmitting ||
|
|
!!passwordError ||
|
|
!!confirmPasswordError ||
|
|
!password ||
|
|
!confirmPassword
|
|
}
|
|
className="flex-1 py-3 rounded-lg cursor-pointer bg-[#C039F6] hover:bg-[#C039F6]/80 text-white font-medium transition-colors disabled:opacity-70 disabled:cursor-not-allowed"
|
|
>
|
|
{isSubmitting ? "Signing up..." : "Sign Up"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{/* Fixed Footer */}
|
|
<div className="flex-shrink-0 p-6 pt-4">
|
|
<div className="mb-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)" }}>
|
|
Already have an account?{" "}
|
|
<Link href="/login" className="text-[#C039F6] hover:text-[#C039F6]/80 transition-colors">
|
|
Sign in
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{showActivationModal && (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
|
data-alt="activation-modal-overlay"
|
|
>
|
|
<div
|
|
className="w-full max-w-xl mx-4 relative rounded-2xl border border-white/15 bg-white/5 backdrop-blur-xl shadow-2xl"
|
|
data-alt="activation-modal"
|
|
>
|
|
<div className="absolute -inset-px rounded-2xl bg-gradient-to-br from-cyan-400/10 to-purple-600/10 pointer-events-none"></div>
|
|
<div className="relative p-6">
|
|
<div className="flex items-center justify-center mb-4">
|
|
<div className="relative">
|
|
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-cyan-400 to-purple-600 blur-xl opacity-30"></div>
|
|
<Mail className="relative z-10 w-10 h-10 text-cyan-300" />
|
|
</div>
|
|
</div>
|
|
<h3 className="text-xl font-semibold text-white text-center mb-2" data-alt="activation-title">Please verify your email to activate your account</h3>
|
|
<p className="text-gray-300 text-center mb-4" data-alt="activation-desc">
|
|
We have sent an activation email to {email || "your email"}; if you don't receive it, please check your spam folder or try again later
|
|
</p>
|
|
|
|
<div className="flex items-center justify-center mb-4 gap-3">
|
|
{(() => {
|
|
const provider = getEmailProviderLink(email);
|
|
if (!provider) return null;
|
|
const domain = email.split("@")[1] || "email";
|
|
return (
|
|
<a
|
|
href={provider}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-white/15 text-gray-100 hover:bg-white/10 transition-colors"
|
|
data-alt="email-provider-link"
|
|
>
|
|
<Mail className="w-4 h-4" />
|
|
<span>Open {domain}</span>
|
|
</a>
|
|
);
|
|
})()}
|
|
</div>
|
|
|
|
{(resendMessage || resendError) && (
|
|
<div className={`text-sm text-center mb-3 ${resendError ? "text-red-300" : "text-green-300"}`} data-alt="resend-feedback">
|
|
{resendError || resendMessage}
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={handleResend}
|
|
disabled={resendCooldown > 0 || resendLoading}
|
|
className="px-4 py-2 rounded-lg border border-white/15 text-white/90 hover:text-white hover:bg-white/10 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
|
data-alt="resend-button"
|
|
>
|
|
{resendLoading
|
|
? "Sending..."
|
|
: resendCooldown > 0
|
|
? `Resend (${resendCooldown}s)`
|
|
: "Resend"}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => router.push("/login")}
|
|
className="px-4 py-2 rounded-lg bg-[#C039F6] hover:bg-[#C039F6]/80 text-white transition-colors"
|
|
data-alt="go-login-button"
|
|
>
|
|
Confirm Activation and Login
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|