Files
gitlab-instance-0a899031_no…/app/vip/components/dashboard/WelcomeBlock.tsx
2026-03-13 03:43:50 -05:00

341 lines
11 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 { useEffect, useState, type FormEvent } from "react";
import { Copyable } from "../Copyable";
import styles from "../vip.module.css";
type WelcomeBlockProps = {
userName: string | null;
userEmail?: string | null;
bindingRequired?: boolean;
};
const DEFAULT_PASSWORD = "12345678";
function getStorage(key: string): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(key);
}
function isAnonymousUserId(value: string | null): value is string {
return !!value && /^user\d+$/.test(value);
}
export function WelcomeBlock({
userName,
userEmail,
bindingRequired = false,
}: WelcomeBlockProps) {
const userId = getStorage("userId");
const storedUserName = getStorage("userName");
const storedUserEmail = getStorage("userEmail");
const emailLinked = getStorage("emailLinked");
const [dbEmail, setDbEmail] = useState<string | null>(null);
const [backendLinkedEmail, setBackendLinkedEmail] = useState<string | null>(null);
const [backendMemosAccount, setBackendMemosAccount] = useState<string | null>(null);
const [email, setEmail] = useState("");
const [checkingLink, setCheckingLink] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const displayName =
storedUserEmail || userEmail || userName || storedUserName || "会员";
const storedLinkedEmail =
storedUserEmail || (storedUserName?.includes("@") ? storedUserName : null);
const memosAccount =
getStorage("memosAccount") ||
backendMemosAccount ||
(isAnonymousUserId(userId) ? userId : null);
const pbEmail = (() => {
try {
const raw = getStorage("pb_user");
if (!raw) return null;
const parsed = JSON.parse(raw) as { email?: string };
return parsed.email ?? null;
} catch {
return null;
}
})();
const alreadyLinked =
emailLinked === "1" ||
!!storedLinkedEmail ||
!!backendLinkedEmail ||
!!pbEmail;
const needLinkEmail =
bindingRequired &&
isAnonymousUserId(userId) &&
!alreadyLinked &&
!checkingLink;
useEffect(() => {
if (!userId || (alreadyLinked && memosAccount)) {
setCheckingLink(false);
return;
}
fetch(`/api/vip/linked-email?user_id=${encodeURIComponent(userId)}`, {
cache: "no-store",
})
.then((res) => res.json())
.then((data) => {
const linkedEmail = data?.email;
const linkedAnonymousUserId =
typeof data?.anonymousUserId === "string" &&
/^user\d+$/.test(data.anonymousUserId)
? data.anonymousUserId
: null;
if (linkedAnonymousUserId && typeof window !== "undefined") {
setBackendMemosAccount(linkedAnonymousUserId);
localStorage.setItem("memosAccount", linkedAnonymousUserId);
}
if (!linkedEmail || typeof window === "undefined") return;
setBackendLinkedEmail(linkedEmail);
localStorage.setItem("emailLinked", "1");
localStorage.setItem("userEmail", linkedEmail);
localStorage.setItem("userName", linkedEmail);
if (bindingRequired) {
window.location.reload();
}
})
.catch(() => {})
.finally(() => setCheckingLink(false));
}, [alreadyLinked, bindingRequired, memosAccount, userId]);
useEffect(() => {
if (storedLinkedEmail || pbEmail || backendLinkedEmail) return;
if (userEmail) {
setDbEmail(userEmail);
return;
}
const applyEmail = (nextEmail: string | null) => {
if (!nextEmail) return;
setDbEmail(nextEmail);
if (typeof window !== "undefined" && emailLinked === "1") {
localStorage.setItem("userEmail", nextEmail);
}
};
fetch("/api/auth/email", { credentials: "include", cache: "no-store" })
.then((res) => res.json())
.then((data) => {
if (data?.email) {
applyEmail(data.email);
return;
}
return fetch("/api/auth/me", {
credentials: "include",
cache: "no-store",
})
.then((res) => res.json())
.then((meData) => applyEmail(meData?.user?.email ?? null));
})
.catch(() => {});
}, [backendLinkedEmail, emailLinked, pbEmail, storedLinkedEmail, userEmail]);
const handleLinkSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!userId || !isAnonymousUserId(userId)) return;
setLoading(true);
setError(null);
try {
const normalizedEmail = email.trim().toLowerCase();
const res = await fetch("/api/auth/link-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: normalizedEmail,
password: DEFAULT_PASSWORD,
anonymousUserId: userId,
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok || !data?.ok || !data?.token || !data?.record?.id) {
setError(data?.error || "绑定邮箱失败");
return;
}
const { pbSaveAuth } = await import("@/app/lib/pocketbase");
pbSaveAuth(data.token, {
id: data.record.id,
email: data.record.email,
});
await fetch("/api/auth/sync-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token: data.token,
record: data.record,
anonymousUserId: userId,
}),
credentials: "include",
});
localStorage.setItem("memosAccount", userId);
localStorage.setItem("userId", data.record.id);
localStorage.setItem("userEmail", data.record.email || normalizedEmail);
localStorage.setItem("userName", data.record.email || normalizedEmail);
localStorage.setItem("emailLinked", "1");
localStorage.setItem("paidType", "vip");
window.location.href = "/";
} catch {
setError("绑定邮箱失败");
} finally {
setLoading(false);
}
};
const displayEmail =
storedLinkedEmail || pbEmail || backendLinkedEmail || dbEmail || userEmail || null;
return (
<>
{needLinkEmail ? (
<section
className={styles.section}
style={{ paddingTop: "1.5rem", paddingBottom: "0.5rem" }}
>
<div className={styles.loginCard} style={{ maxWidth: "420px" }}>
<div
style={{
fontSize: "1rem",
fontWeight: 600,
marginBottom: "0.35rem",
color: "var(--foreground)",
}}
>
VIP
</div>
<p
style={{
fontSize: "0.85rem",
color: "var(--muted-foreground)",
marginBottom: "0.9rem",
}}
>
{" "}
{DEFAULT_PASSWORD}
</p>
<form onSubmit={handleLinkSubmit}>
<input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
placeholder="your@email.com"
required
disabled={loading}
style={{
width: "100%",
padding: "0.75rem 1rem",
fontSize: "1rem",
border: "1px solid var(--border)",
borderRadius: "8px",
background: "var(--background)",
color: "var(--foreground)",
marginBottom: "0.75rem",
}}
/>
{error ? (
<p
style={{
fontSize: "0.85rem",
color: "#ef4444",
marginBottom: "0.75rem",
}}
>
{error}
</p>
) : null}
<button
type="submit"
disabled={loading}
className={`${styles.btnPrimary} ${styles.btnLarge}`}
style={{ width: "100%" }}
>
{loading ? "处理中..." : "绑定邮箱并继续"}
</button>
</form>
</div>
</section>
) : null}
<section className={styles.section} style={{ paddingTop: "1.5rem" }}>
<h1
className="animate__animated animate__fadeInDown"
style={{
fontSize: "clamp(1.5rem, 4vw, 2rem)",
fontWeight: 600,
margin: 0,
animationDelay: "0.1s",
animationFillMode: "both",
}}
>
<Copyable text={displayName}>{displayName}</Copyable>
</h1>
<p
className="animate__animated animate__fadeInDown"
style={{
fontSize: "0.95rem",
color: "var(--muted-foreground)",
margin: "0.25rem 0 0",
animationDelay: "0.15s",
animationFillMode: "both",
}}
>
</p>
<div className={styles.loginCard}>
<div className={styles.loginCardTitle}></div>
<div className={styles.loginCardRow}>
<span className={styles.loginCardLabel}></span>
<Copyable text="https://qun.hackrobot.cn">
qun.hackrobot.cn
</Copyable>
<a
href="https://qun.hackrobot.cn"
target="_blank"
rel="noopener noreferrer"
className={styles.loginCardLink}
>
</a>
</div>
<div className={styles.loginCardRow}>
<span className={styles.loginCardLabel}></span>
<Copyable text={memosAccount || ""}>
<span className={styles.loginCardValue}>{memosAccount || "-"}</span>
</Copyable>
</div>
<div className={styles.loginCardRow}>
<span className={styles.loginCardLabel}></span>
<Copyable text={DEFAULT_PASSWORD}>
<span className={styles.loginCardValue}>{DEFAULT_PASSWORD}</span>
</Copyable>
</div>
<div className={styles.loginCardRow}>
<span className={styles.loginCardLabel}></span>
<Copyable text={displayEmail || ""}>
<span className={styles.loginCardValue}>{displayEmail || "-"}</span>
</Copyable>
</div>
</div>
</section>
</>
);
}