forked from 77media/video-flow
提醒用户激活流程
This commit is contained in:
parent
9d730de2ca
commit
c1b55b6827
@ -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
|
||||||
@ -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}...`,
|
||||||
|
|||||||
@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
17
lib/auth.ts
17
lib/auth.ts
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user