This commit is contained in:
eric
2026-03-12 03:54:50 -05:00
parent 170a9939aa
commit 40230dd646
20 changed files with 645 additions and 95 deletions

View File

@@ -47,6 +47,15 @@ export default function AuthModal({ isOpen, onClose, onSuccess }: AuthModalProps
result = await pbLogin(email, password);
}
pbSaveAuth(result.token, result.record);
try {
await fetch("/api/auth/sync-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: result.token, record: result.record }),
});
} catch {
/* ignore */
}
onSuccess(email);
onClose();
setEmail("");

View File

@@ -3,6 +3,14 @@
import { useState } from "react";
import { Link, useLocale } from "@/i18n/navigation";
import { useTranslation } from "@/i18n/navigation";
import {
getPayEnv,
getRecommendedChannel,
mustUseWxPay,
redirectToPay,
getDeviceFromEnv,
} from "@/app/lib/payment";
import type { PayChannel, PayEnv } from "@/app/lib/payment";
const MEDIA_NAMES_ZH = ["36氪", "少数派", "极客公园", "虎嗅", "创业邦", "澎湃新闻"];
const MEDIA_NAMES_EN = ["36Kr", "sspai", "GeekPark", "Huxiu", "Cyzone", "The Paper"];
@@ -34,11 +42,49 @@ export default function HeroSection() {
const { t: tHero } = useTranslation("hero");
const locale = useLocale();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleJoin = (e: React.FormEvent) => {
const handleJoin = async (e: React.FormEvent) => {
e.preventDefault();
if (email.trim()) {
alert(locale === "zh" ? `欢迎加入游牧中国!我们将发送确认邮件到 ${email}` : `Welcome! We'll send a confirmation email to ${email}`);
const em = email.trim();
if (!em) {
setError(locale === "zh" ? "请输入邮箱" : "Please enter your email");
return;
}
setError(null);
setLoading(true);
try {
const res = await fetch("/api/meetup/ensure-user", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: em, password: password || undefined }),
credentials: "include",
});
const data = await res.json();
if (!res.ok || !data?.ok) {
throw new Error(data?.error || (locale === "zh" ? "操作失败" : "Operation failed"));
}
await fetch("/api/auth/sync-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: data.token, record: data.record }),
});
const returnUrl = typeof window !== "undefined" ? `${window.location.origin}/${locale}/join/paid` : "/zh/join/paid";
const env: PayEnv = typeof window !== "undefined" ? (navigator.userAgent.includes("MicroMessenger") ? "wechat" : "pc") : "pc";
const channel: PayChannel = mustUseWxPay(env) ? "wxpay" : getRecommendedChannel(env);
redirectToPay({
user_id: data.user_id,
return_url: returnUrl,
channel,
device: getDeviceFromEnv(env),
useJoinConfig: true,
});
} catch (err) {
setError(err instanceof Error ? err.message : "操作失败");
} finally {
setLoading(false);
}
};
@@ -151,18 +197,29 @@ export default function HeroSection() {
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onChange={(e) => { setEmail(e.target.value); setError(null); }}
placeholder={tHero("emailPlaceholder")}
className="w-full border border-gray-200 dark:border-gray-700 rounded-lg px-4 py-3 text-sm placeholder-gray-400 dark:placeholder-gray-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20 focus:border-[#ff4d4f] transition-colors mb-3"
/>
<input
type="password"
value={password}
onChange={(e) => { setPassword(e.target.value); setError(null); }}
placeholder={locale === "zh" ? "密码(老用户必填)" : "Password (required for existing users)"}
className="w-full border border-gray-200 dark:border-gray-700 rounded-lg px-4 py-3 text-sm placeholder-gray-400 dark:placeholder-gray-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20 focus:border-[#ff4d4f] transition-colors mb-3"
/>
{error && (
<p className="mb-2 text-sm text-red-600 dark:text-red-400">{error}</p>
)}
<button
type="submit"
className="w-full bg-[#ff4d4f] hover:bg-[#ff3333] dark:hover:bg-[#ff5c5e] text-white py-3 rounded-lg text-sm font-semibold transition-colors active:scale-[0.98]"
disabled={loading}
className="w-full bg-[#ff4d4f] hover:bg-[#ff3333] dark:hover:bg-[#ff5c5e] text-white py-3 rounded-lg text-sm font-semibold transition-colors active:scale-[0.98] disabled:opacity-70"
>
{tHero("ctaJoin")}
{loading ? (locale === "zh" ? "处理中…" : "Processing…") : tHero("ctaJoin")}
</button>
<p className="text-xs text-gray-400 dark:text-gray-500 text-center mt-3">
{tHero("loginHint")}
{locale === "zh" ? "新用户将自动注册,支付成功后需修改默认密码" : "New users auto-register; change default password after payment"}
</p>
</form>
</div>

View File

@@ -22,11 +22,28 @@ export default function NavbarComponent() {
const navLinks = getNavLinks();
useEffect(() => {
setUserEmail(getStoredUserEmail());
let mounted = true;
(async () => {
try {
const res = await fetch("/api/auth/me");
const data = await res.json();
if (!mounted) return;
if (data?.user && data?.token) {
const { pbSaveAuth } = await import("@/app/lib/pocketbase");
pbSaveAuth(data.token, { id: data.user.id, email: data.user.email });
setUserEmail(data.user.email);
return;
}
} catch {
/* ignore */
}
if (mounted) setUserEmail(getStoredUserEmail());
})();
return () => { mounted = false; };
}, []);
const handleLogout = useCallback(() => {
pbLogout();
const handleLogout = useCallback(async () => {
await pbLogout();
setUserEmail(null);
}, []);
@@ -104,14 +121,7 @@ export default function NavbarComponent() {
{t("logout")}
</button>
</div>
) : (
<button
onClick={() => setAuthOpen(true)}
className="hidden md:block text-xs sm:text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 px-2 sm:px-3 py-2 rounded-full hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors whitespace-nowrap"
>
{t("loginRegister")}
</button>
)}
) : null}
<button
type="button"
onClick={() => router.replace(pathname, { locale: locale === "zh" ? "en" : "zh" })}
@@ -177,14 +187,7 @@ export default function NavbarComponent() {
{t("logout")}
</button>
</div>
) : (
<button
onClick={() => { setMobileOpen(false); setAuthOpen(true); }}
className="w-full text-left px-4 py-3 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
>
{t("loginRegister")}
</button>
)}
) : null}
</div>
)}

View File

@@ -85,7 +85,7 @@ export default memo(function SiteMenu({
{
titleKey: "other",
items: [
{ labelKey: "digitalNomadGuide", href: "https://digital.hackrobot.cn/", icon: "📖" },
{ labelKey: "digitalNomadGuide", href: process.env.NEXT_PUBLIC_DIGITAL_URL || "https://digital.hackrobot.cn/", icon: "📖" },
{ labelKey: "remoteJobs", href: "#", icon: "💼" },
{ labelKey: "coworking", href: "#", icon: "🏢" },
],