提醒用户激活流程

This commit is contained in:
北枳 2025-09-16 16:48:01 +08:00
parent 9d730de2ca
commit c1b55b6827
6 changed files with 159 additions and 13 deletions

View File

@ -1,10 +1,8 @@
NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com
# NEXT_PUBLIC_JAVA_URL = http://192.168.120.83:8080
NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
NEXT_PUBLIC_CUT_URL = https://smartcut.huiying.video NEXT_PUBLIC_CUT_URL = https://smartcut.huiying.video
# NEXT_PUBLIC_JAVA_URL = https://auth.movieflow.ai
# NEXT_PUBLIC_BASE_URL = https://api.video.movieflow.ai
# NEXT_PUBLIC_CUT_URL = https://smartcut.movieflow.ai
# 失败率 # 失败率
NEXT_PUBLIC_ERROR_CONFIG = 0.1 NEXT_PUBLIC_ERROR_CONFIG = 0.1

View File

@ -656,7 +656,8 @@ export interface ShotVideo {
// 执行loading文字映射 // 执行loading文字映射
export const LOADING_TEXT_MAP = { export const LOADING_TEXT_MAP = {
initializing: 'initializing...', getInfo: 'Getting information...',
initializing: 'initializing project...',
script: 'Generating script...', script: 'Generating script...',
getSketchStatus: 'Getting sketch status...', getSketchStatus: 'Getting sketch status...',
sketch: (count: number, total: number) => `Generating sketch ${count}/${total}...`, sketch: (count: number, total: number) => `Generating sketch ${count}/${total}...`,

View File

@ -3,9 +3,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { signInWithGoogle, registerUser } from "@/lib/auth"; import { signInWithGoogle, registerUser, sendVerificationLink } from "@/lib/auth";
import { GradientText } from "@/components/ui/gradient-text"; import { GradientText } from "@/components/ui/gradient-text";
import { Eye, EyeOff } from "lucide-react"; import { Eye, EyeOff, Mail } from "lucide-react";
export default function SignupPage() { export default function SignupPage() {
const [name, setName] = useState(""); const [name, setName] = useState("");
@ -20,6 +20,11 @@ export default function SignupPage() {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [agreeToTerms, setAgreeToTerms] = useState(true); 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 router = useRouter(); const router = useRouter();
/** Password validation function with English prompts */ /** Password validation function with English prompts */
@ -36,6 +41,52 @@ export default function SignupPage() {
return ""; 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 */ /** Handle Terms of Service click */
const handleTermsClick = () => { const handleTermsClick = () => {
window.open("/Terms", "_blank"); window.open("/Terms", "_blank");
@ -120,8 +171,9 @@ export default function SignupPage() {
inviteCode: inviteCode || undefined, inviteCode: inviteCode || undefined,
}); });
// Redirect to login page after successful registration // Show activation modal instead of redirecting to login
router.push("/login?registered=true"); setShowActivationModal(true);
setResendCooldown(60);
} catch (error: any) { } catch (error: any) {
console.error("Signup error:", error); console.error("Signup error:", error);
setFormError(error.message||error.msg || "Registration failed, please try again"); setFormError(error.message||error.msg || "Registration failed, please try again");
@ -131,6 +183,7 @@ export default function SignupPage() {
}; };
return ( return (
<>
<div className="min-h-screen relative overflow-hidden"> <div className="min-h-screen relative overflow-hidden">
{/* 背景视频 */} {/* 背景视频 */}
<video <video
@ -374,5 +427,81 @@ export default function SignupPage() {
</div> </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>
)}
</>
); );
} }

View File

@ -214,10 +214,10 @@ export function TaskInfo({
{taskObject?.title ? ( {taskObject?.title ? (
<> <>
<span> <span>
{taskObject?.title || 'initializing project...'} {taskObject?.title || currentLoadingText}
</span> </span>
</> </>
) : 'initializing project...'} ) : currentLoadingText}
</div> </div>
{/* 主题 彩色标签tags */} {/* 主题 彩色标签tags */}

View File

@ -63,14 +63,14 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
note: '' note: ''
} }
}); });
let loadingText: any = useRef(LOADING_TEXT_MAP.initializing); let loadingText: any = useRef(LOADING_TEXT_MAP.getInfo);
const errorConfig = Number(process.env.NEXT_PUBLIC_ERROR_CONFIG); const errorConfig = Number(process.env.NEXT_PUBLIC_ERROR_CONFIG);
// 更新 taskObject 的类型 // 更新 taskObject 的类型
const [taskObject, setTaskObject] = useState<TaskObject>(tempTaskObject.current); const [taskObject, setTaskObject] = useState<TaskObject>(tempTaskObject.current);
const [currentSketchIndex, setCurrentSketchIndex] = useState(0); const [currentSketchIndex, setCurrentSketchIndex] = useState(0);
const [currentLoadingText, setCurrentLoadingText] = useState('initializing project...'); const [currentLoadingText, setCurrentLoadingText] = useState<string>(LOADING_TEXT_MAP.getInfo);
const [dataLoadError, setDataLoadError] = useState<string | null>(null); const [dataLoadError, setDataLoadError] = useState<string | null>(null);
const [needStreamData, setNeedStreamData] = useState(false); const [needStreamData, setNeedStreamData] = useState(false);
const [isPauseWorkFlow, setIsPauseWorkFlow] = useState(false); const [isPauseWorkFlow, setIsPauseWorkFlow] = useState(false);
@ -535,7 +535,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
originalText: '', originalText: '',
isLoading: true isLoading: true
}); });
setCurrentLoadingText('initializing project...'); setCurrentLoadingText(LOADING_TEXT_MAP.getInfo);
// 获取剧集详情 // 获取剧集详情
const response = await detailScriptEpisodeNew({ project_id: episodeId }); const response = await detailScriptEpisodeNew({ project_id: episodeId });
@ -554,6 +554,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
// 设置标题 // 设置标题
if (!name) { if (!name) {
setCurrentLoadingText(LOADING_TEXT_MAP.initializing);
// 如果没有标题,轮询获取 // 如果没有标题,轮询获取
const titleResponse = await getScriptTitle({ project_id: episodeId }); const titleResponse = await getScriptTitle({ project_id: episodeId });
console.log('titleResponse', titleResponse); console.log('titleResponse', titleResponse);

View File

@ -369,3 +369,20 @@ export const registerUser = async ({
throw error; throw error;
} }
}; };
export const sendVerificationLink = async (email: string) => {
try {
const response = await fetch(`${JAVA_BASE_URL}/api/user/sendVerificationLink?email=${email}`);
const data = await response.json();
if(!data.success){
throw new Error(data.message||data.msg)
}
return data as {
success: boolean;
};
} catch (error) {
console.error('Send verification link failed:', error);
throw error;
}
};