Polish three rings: connect auth gates, explore shortcuts, grow nav and forms.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric 2026-08-30 21:37:57 -05:00
parent 2ee0f3bf77
commit ea030275ce
23 changed files with 274 additions and 26 deletions

View File

@ -1,4 +1,5 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { api } from "@/lib/api";
import SiteShell from "@/components/SiteShell"; import SiteShell from "@/components/SiteShell";
import AiAssistantClient from "@/components/AiAssistantClient"; import AiAssistantClient from "@/components/AiAssistantClient";
@ -7,10 +8,13 @@ export const metadata: Metadata = {
description: "智能推荐下一站游民城市", description: "智能推荐下一站游民城市",
}; };
export default function AiPage() { export const revalidate = 60;
export default async function AiPage() {
const destinations = await api.getDestinations().catch(() => []);
return ( return (
<SiteShell showFooter> <SiteShell showFooter>
<AiAssistantClient /> <AiAssistantClient destinations={destinations} />
</SiteShell> </SiteShell>
); );
} }

View File

@ -8,6 +8,15 @@ export const metadata: Metadata = {
}; };
const LOGS = [ const LOGS = [
{
date: "2026-08-30",
tag: "三环登录与导航",
items: [
"Connect:个人中心三环快捷入口、匹配加载态、发起活动/发赏金登录门、私信与登录提示",
"Explore:全站快捷键、首页 hash 锚点、对比空态、AI 推荐真正写入计划",
"Grow:导航补全服务/投稿、表单校验、学院空态与 VIP 课时直达开通",
],
},
{ {
date: "2026-08-30", date: "2026-08-30",
tag: "三环可用深化", tag: "三环可用深化",

View File

@ -10903,6 +10903,14 @@ img { max-width: 100%; display: block; }
font-size: 0.95rem; font-size: 0.95rem;
} }
.digital-lesson-list li a.digital-lesson-locked {
opacity: 0.85;
}
.digital-lesson-list li a.digital-lesson-locked:hover .digital-lesson-meta {
color: var(--accent-3);
}
.digital-lesson-meta { .digital-lesson-meta {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-muted); color: var(--text-muted);

View File

@ -73,7 +73,7 @@ export default function ProfilePage() {
useEffect(() => { useEffect(() => {
if (!token) { if (!token) {
router.push("/login"); router.push("/login?next=/profile");
return; return;
} }
setLoading(true); setLoading(true);
@ -291,8 +291,13 @@ export default function ProfilePage() {
<Link href="/#destinations" className="quick-card">🌍 浏览目的地</Link> <Link href="/#destinations" className="quick-card">🌍 浏览目的地</Link>
<Link href="/plan" className="quick-card">🗓️ 旅居计划</Link> <Link href="/plan" className="quick-card">🗓️ 旅居计划</Link>
<Link href="/compare" className="quick-card">⚖️ 城市对比</Link> <Link href="/compare" className="quick-card">⚖️ 城市对比</Link>
<Link href="/#visa" className="quick-card">📋 签证指南</Link> <Link href="/next-stop" className="quick-card">🧭 智能下一站</Link>
<Link href="/#blog" className="quick-card">📝 阅读博客</Link> <Link href="/meetups" className="quick-card">🎉 游民活动</Link>
<Link href="/dating" className="quick-card">💕 游民匹配</Link>
<Link href="/chat" className="quick-card">✉️ 私信</Link>
<Link href="/notifications" className="quick-card">🔔 通知</Link>
<Link href={vip ? "/join" : "/join"} className="quick-card">{vip ? "✨ VIP 会员" : "✨ 开通 VIP"}</Link>
<Link href="/digital" className="quick-card">🎓 游民学院</Link>
<Link href="/tools" className="quick-card">🛠️ 工具箱</Link> <Link href="/tools" className="quick-card">🛠️ 工具箱</Link>
</div> </div>
</section> </section>

View File

@ -2,9 +2,12 @@
import { useState } from "react"; import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
import { useI18n } from "@/lib/i18n"; import { useI18n } from "@/lib/i18n";
import { useToast } from "@/lib/toast"; import { useToast } from "@/lib/toast";
import { mergeDestinationsIntoTrip } from "@/lib/tripActions";
import type { Destination } from "@/lib/types";
import RingNext from "@/components/RingNext"; import RingNext from "@/components/RingNext";
import { useRingSteps } from "@/lib/rings"; import { useRingSteps } from "@/lib/rings";
@ -14,9 +17,14 @@ const SUGGESTIONS = [
"清迈和巴厘岛怎么选?", "清迈和巴厘岛怎么选?",
]; ];
export default function AiAssistantClient() { interface Props {
destinations: Destination[];
}
export default function AiAssistantClient({ destinations }: Props) {
const { t } = useI18n(); const { t } = useI18n();
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter();
const rings = useRingSteps(); const rings = useRingSteps();
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const [reply, setReply] = useState(""); const [reply, setReply] = useState("");
@ -43,6 +51,24 @@ export default function AiAssistantClient() {
} }
}; };
const addToPlan = () => {
const dests = cities
.map((c) => destinations.find((d) => d.slug === c.slug))
.filter((d): d is Destination => Boolean(d));
if (dests.length === 0) {
toast("推荐城市暂无完整数据,请从详情页加入计划", "info");
router.push("/plan");
return;
}
const { added, skipped } = mergeDestinationsIntoTrip(dests, 1);
if (added === 0) {
toast(skipped ? t.compare.alreadyInPlan : "未能写入计划", "info");
} else {
toast(`${t.compare.addedN} ${added} ${t.compare.citiesUnit}`);
}
router.push("/plan");
};
return ( return (
<div className="community-page"> <div className="community-page">
<div className="container"> <div className="container">
@ -93,9 +119,11 @@ export default function AiAssistantClient() {
<Link href="/compare" className="btn btn-sm"> <Link href="/compare" className="btn btn-sm">
城市对比 城市对比
</Link> </Link>
<Link href="/plan" className="btn btn-sm"> {cities.length > 0 && (
写入计划 <button type="button" className="btn btn-sm btn-primary" onClick={addToPlan}>
</Link> 写入计划
</button>
)}
</div> </div>
</div> </div>
)} )}

View File

@ -45,6 +45,7 @@ export default function ChatClient() {
<div className="container"> <div className="container">
<div className="dating-gate reveal"> <div className="dating-gate reveal">
<h2>{t.chat.loginTitle}</h2> <h2>{t.chat.loginTitle}</h2>
<p>{t.chat.loginDesc}</p>
<Link href="/login?next=/chat" className="btn btn-primary">{t.nav.login}</Link> <Link href="/login?next=/chat" className="btn btn-primary">{t.nav.login}</Link>
</div> </div>
</div> </div>

View File

@ -62,6 +62,7 @@ export default function ChatThreadClient({ convId }: { convId: string }) {
<div className="container"> <div className="container">
<div className="dating-gate reveal"> <div className="dating-gate reveal">
<h2>{t.chat.loginTitle}</h2> <h2>{t.chat.loginTitle}</h2>
<p>{t.chat.loginDesc}</p>
<Link href={`/login?next=/chat/${convId}`} className="btn btn-primary"> <Link href={`/login?next=/chat/${convId}`} className="btn btn-primary">
{t.nav.login} {t.nav.login}
</Link> </Link>

View File

@ -125,9 +125,23 @@ export default function CommunityHubClient({ discussions, featured }: Props) {
{filtered.length === 0 && ( {filtered.length === 0 && (
<div className="notif-empty reveal"> <div className="notif-empty reveal">
<p className="dest-empty">{t.community.empty}</p> <p className="dest-empty">{t.community.empty}</p>
<Link href="/community/new" className="btn btn-primary btn-sm"> <div className="dating-empty-actions">
{t.community.newTopic} {(category !== "全部" || q.trim()) && (
</Link> <button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => {
setCategory("全部");
setQ("");
}}
>
{t.meetups.all}
</button>
)}
<Link href="/community/new" className="btn btn-primary btn-sm">
{t.community.newTopic}
</Link>
</div>
</div> </div>
)} )}

View File

@ -145,7 +145,11 @@ export default function CompareClient({ destinations }: Props) {
<div className="plan-empty compare-page-empty"> <div className="plan-empty compare-page-empty">
<span className="plan-empty-icon">⚖️</span> <span className="plan-empty-icon">⚖️</span>
<p>{t.compare.needMore} {Math.max(0, 2 - selected.length)} {t.compare.needMoreSuffix}</p> <p>{t.compare.needMore} {Math.max(0, 2 - selected.length)} {t.compare.needMoreSuffix}</p>
<Link href="/#destinations" className="btn btn-ghost">{t.compare.backDest}</Link> <div className="dating-empty-actions">
<Link href="/#destinations" className="btn btn-ghost">{t.compare.backDest}</Link>
<Link href="/next-stop" className="btn">{t.nav.nextStop}</Link>
<Link href="/ai" className="btn btn-ghost">{t.nav.ai}</Link>
</div>
</div> </div>
) : ( ) : (
<> <>

View File

@ -8,11 +8,15 @@ import { useAuth } from "@/lib/auth";
import { useToast } from "@/lib/toast"; import { useToast } from "@/lib/toast";
import { useI18n } from "@/lib/i18n"; import { useI18n } from "@/lib/i18n";
import RingNext from "@/components/RingNext";
import { useRingSteps } from "@/lib/rings";
export default function ContentSubmitClient() { export default function ContentSubmitClient() {
const { token } = useAuth(); const { token } = useAuth();
const router = useRouter(); const router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
const { t } = useI18n(); const { t } = useI18n();
const rings = useRingSteps();
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
const [notes, setNotes] = useState(""); const [notes, setNotes] = useState("");
@ -29,6 +33,10 @@ export default function ContentSubmitClient() {
toast("请填写更完整的标题", "info"); toast("请填写更完整的标题", "info");
return; return;
} }
if ((contentType === "article" || contentType === "video") && !/^https?:\/\/.+/.test(url.trim())) {
toast("文章/视频请填写有效链接(http/https)", "info");
return;
}
setBusy(true); setBusy(true);
try { try {
await api.submitContent( await api.submitContent(
@ -113,6 +121,7 @@ export default function ContentSubmitClient() {
)} )}
</div> </div>
)} )}
<RingNext steps={rings.afterGigs} />
</div> </div>
</div> </div>
); );

View File

@ -160,7 +160,9 @@ export default function DatingClient() {
</div> </div>
)} )}
{loading && joined && <p className="dest-empty reveal">加载候选人…</p>} {loading && (
<p className="dest-empty reveal">{joined ? "加载候选人…" : "加载匹配资料…"}</p>
)}
{top && ( {top && (
<div className="dating-card reveal"> <div className="dating-card reveal">

View File

@ -39,7 +39,14 @@ export default function DigitalCourseClient({ course }: { course: DigitalCourse
)} )}
</div> </div>
{course.modules.length === 0 ? ( {course.modules.length === 0 ? (
<p className="dest-empty">课程内容准备中</p> <div className="notif-empty reveal">
<p className="dest-empty">课程内容准备中</p>
<div className="dating-empty-actions">
<Link href="/digital" className="btn">{t.digital.back}</Link>
<Link href="/gigs" className="btn btn-ghost">{t.nav.gigs}</Link>
<Link href="/join" className="btn btn-primary">{t.nav.join}</Link>
</div>
</div>
) : ( ) : (
<div className="digital-modules reveal"> <div className="digital-modules reveal">
{course.modules.map((mod, mi) => ( {course.modules.map((mod, mi) => (
@ -50,16 +57,22 @@ export default function DigitalCourseClient({ course }: { course: DigitalCourse
const locked = !lesson.free && !vip; const locked = !lesson.free && !vip;
return ( return (
<li key={lesson.title}> <li key={lesson.title}>
<Link href={`/digital/course/${mi}/${li}`}> {locked ? (
<span> <Link href="/join" className="digital-lesson-locked">
{locked ? "🔒 " : ""} <span>🔒 {lesson.title}</span>
{lesson.title} <span className="digital-lesson-meta">
</span> {lesson.duration} · VIP · 开通解锁
<span className="digital-lesson-meta"> </span>
{lesson.duration} </Link>
{lesson.free ? ` · ${t.digital.free}` : " · VIP"} ) : (
</span> <Link href={`/digital/course/${mi}/${li}`}>
</Link> <span>{lesson.title}</span>
<span className="digital-lesson-meta">
{lesson.duration}
{lesson.free ? ` · ${t.digital.free}` : " · VIP"}
</span>
</Link>
)}
</li> </li>
); );
})} })}

View File

@ -6,11 +6,14 @@ import { api } from "@/lib/api";
import { useAuth } from "@/lib/auth"; import { useAuth } from "@/lib/auth";
import { useToast } from "@/lib/toast"; import { useToast } from "@/lib/toast";
import { useI18n } from "@/lib/i18n"; import { useI18n } from "@/lib/i18n";
import RingNext from "@/components/RingNext";
import { useRingSteps } from "@/lib/rings";
export default function FeedbackClient() { export default function FeedbackClient() {
const { token } = useAuth(); const { token } = useAuth();
const { toast } = useToast(); const { toast } = useToast();
const { t } = useI18n(); const { t } = useI18n();
const rings = useRingSteps();
const [content, setContent] = useState(""); const [content, setContent] = useState("");
const [category, setCategory] = useState("general"); const [category, setCategory] = useState("general");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -79,6 +82,7 @@ export default function FeedbackClient() {
{!token && <p className="section-desc">未登录也可提交;登录后便于我们回复你</p>} {!token && <p className="section-desc">未登录也可提交;登录后便于我们回复你</p>}
</div> </div>
)} )}
<RingNext steps={rings.afterGigs} />
</div> </div>
</div> </div>
); );

View File

@ -61,6 +61,9 @@ export default function Footer() {
<h4>{t.footer.grow}</h4> <h4>{t.footer.grow}</h4>
<Link href="/digital">{t.footer.digital}</Link> <Link href="/digital">{t.footer.digital}</Link>
<Link href="/gigs">{t.footer.gigs}</Link> <Link href="/gigs">{t.footer.gigs}</Link>
<Link href="/services">{t.nav.services}</Link>
<Link href="/submit">{t.submit.tag}</Link>
<Link href="/feedback">{t.nav.feedback}</Link>
<Link href="/tools">{t.footer.toolkit}</Link> <Link href="/tools">{t.footer.toolkit}</Link>
<Link href="/join">{t.footer.join}</Link> <Link href="/join">{t.footer.join}</Link>
</div> </div>

View File

@ -19,6 +19,21 @@ export default function GigPostClient() {
const [deadline, setDeadline] = useState(""); const [deadline, setDeadline] = useState("");
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
if (!token) {
return (
<div className="community-page">
<div className="container">
<nav className="detail-nav"><Link href="/gigs">{t.gigs.back}</Link></nav>
<div className="dating-gate reveal">
<h2>{t.gigs.postTitle}</h2>
<p>{t.gigs.loginFirst}</p>
<Link href="/login?next=/gigs/post" className="btn btn-primary">{t.nav.login}</Link>
</div>
</div>
</div>
);
}
const submit = async () => { const submit = async () => {
if (!token) { if (!token) {
router.push("/login?next=/gigs/post"); router.push("/login?next=/gigs/post");

View File

@ -53,6 +53,17 @@ export default function HomeClient(props: Props) {
const [chromeReady, setChromeReady] = useState(false); const [chromeReady, setChromeReady] = useState(false);
const rings = useRingSteps(); const rings = useRingSteps();
useEffect(() => {
const hash = window.location.hash.slice(1);
if (!hash) return;
const scrollToHash = () => {
const el = document.getElementById(hash);
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
};
const t = window.setTimeout(scrollToHash, 400);
return () => window.clearTimeout(t);
}, []);
useEffect(() => { useEffect(() => {
const onMatcher = () => setMatcherOpen(true); const onMatcher = () => setMatcherOpen(true);
window.addEventListener("open-matcher", onMatcher); window.addEventListener("open-matcher", onMatcher);

View File

@ -122,7 +122,17 @@ export default function MeetupLiveClient({ meetupId, title }: { meetupId: string
<iframe title="chat" src={session.chatUrl} className="live-iframe live-chat" /> <iframe title="chat" src={session.chatUrl} className="live-iframe live-chat" />
)} )}
{!session.videoUrl && !session.chatUrl && ( {!session.videoUrl && !session.chatUrl && (
<p className="dest-empty">直播链接尚未配置</p> <div className="notif-empty">
<p className="dest-empty">直播链接尚未配置</p>
<div className="dating-empty-actions">
<Link href="/meetups" className="btn btn-primary">
{t.live.back}
</Link>
<Link href="/feedback" className="btn btn-ghost">
{t.nav.feedback}
</Link>
</div>
</div>
)} )}
</div> </div>
</div> </div>

View File

@ -29,6 +29,21 @@ export default function MeetupsHostClient() {
const set = (k: string, v: string | number) => setForm((f) => ({ ...f, [k]: v })); const set = (k: string, v: string | number) => setForm((f) => ({ ...f, [k]: v }));
if (!token) {
return (
<div className="meetups-page">
<div className="container">
<nav className="detail-nav"><Link href="/meetups">← {t.meetups.title}</Link></nav>
<div className="dating-gate reveal">
<h2>{t.meetups.hostTitle}</h2>
<p>{t.meetups.loginDesc}</p>
<Link href="/login?next=/meetups/host" className="btn btn-primary">{t.nav.login}</Link>
</div>
</div>
</div>
);
}
const submit = async () => { const submit = async () => {
if (!token) { if (!token) {
router.push("/login?next=/meetups/host"); router.push("/login?next=/meetups/host");

View File

@ -58,11 +58,15 @@ export default function Navbar() {
{ href: "/community", label: t.nav.community, section: "community" }, { href: "/community", label: t.nav.community, section: "community" },
{ href: "/dating", label: t.nav.dating, section: "dating" }, { href: "/dating", label: t.nav.dating, section: "dating" },
{ href: "/chat", label: t.nav.chat, section: "chat" }, { href: "/chat", label: t.nav.chat, section: "chat" },
{ href: "/notifications", label: t.nav.notifications, section: "notifications" },
{ href: "/join", label: t.nav.join, section: "join" },
]; ];
const grow: NavItem[] = [ const grow: NavItem[] = [
{ href: "/digital", label: t.nav.digital, section: "digital" }, { href: "/digital", label: t.nav.digital, section: "digital" },
{ href: "/gigs", label: t.nav.gigs, section: "gigs" }, { href: "/gigs", label: t.nav.gigs, section: "gigs" },
{ href: "/services", label: t.nav.services, section: "services" },
{ href: "/submit", label: t.submit.tag, section: "submit" },
{ href: "/tools", label: t.nav.tools, section: "tools" }, { href: "/tools", label: t.nav.tools, section: "tools" },
{ href: "/book", label: t.nav.ebook, section: "ebook" }, { href: "/book", label: t.nav.ebook, section: "ebook" },
]; ];

View File

@ -30,6 +30,10 @@ export default function ServicesClient({ services }: { services: ServiceItem[] }
toast("请填写姓名、邮箱和需求说明", "info"); toast("请填写姓名、邮箱和需求说明", "info");
return; return;
} }
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(f.email.trim())) {
toast("请填写有效邮箱地址", "info");
return;
}
setBusyId(serviceId); setBusyId(serviceId);
try { try {
await api.submitServiceLead(serviceId, { await api.submitServiceLead(serviceId, {

View File

@ -1,12 +1,19 @@
"use client"; "use client";
import { useEffect } from "react"; import { useEffect } from "react";
import dynamic from "next/dynamic";
import { usePathname, useRouter } from "next/navigation";
import Navbar from "./Navbar"; import Navbar from "./Navbar";
import ParticleCanvas from "./ParticleCanvas"; import ParticleCanvas from "./ParticleCanvas";
import Footer from "./Footer"; import Footer from "./Footer";
import GlobalSearch from "./GlobalSearch"; import GlobalSearch from "./GlobalSearch";
const KeyboardShortcuts = dynamic(() => import("./KeyboardShortcuts"), { ssr: false });
export default function SiteShell({ children, showFooter = false }: { children: React.ReactNode; showFooter?: boolean }) { export default function SiteShell({ children, showFooter = false }: { children: React.ReactNode; showFooter?: boolean }) {
const pathname = usePathname();
const router = useRouter();
useEffect(() => { useEffect(() => {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => (entries) =>
@ -35,6 +42,15 @@ export default function SiteShell({ children, showFooter = false }: { children:
<ParticleCanvas /> <ParticleCanvas />
<Navbar /> <Navbar />
<GlobalSearch /> <GlobalSearch />
<KeyboardShortcuts
onOpenMatcher={() => {
if (pathname === "/") {
window.dispatchEvent(new Event("open-matcher"));
} else {
router.push("/next-stop");
}
}}
/>
{children} {children}
{showFooter && <Footer />} {showFooter && <Footer />}
</> </>

View File

@ -7,6 +7,14 @@ type LoginDict = {
hintCompare: string; hintCompare: string;
hintTools: string; hintTools: string;
hintDestination: string; hintDestination: string;
hintDating: string;
hintChat: string;
hintMeetups: string;
hintJoin: string;
hintNotifications: string;
hintGigs: string;
hintCommunity: string;
hintSubmit: string;
redirectTo: string; redirectTo: string;
planMergeNote: string; planMergeNote: string;
destHome: string; destHome: string;
@ -15,6 +23,14 @@ type LoginDict = {
destCompare: string; destCompare: string;
destTools: string; destTools: string;
destDestination: string; destDestination: string;
destDating: string;
destChat: string;
destMeetups: string;
destJoin: string;
destNotifications: string;
destGigs: string;
destCommunity: string;
destSubmit: string;
submitLoginDefault: string; submitLoginDefault: string;
submitLoginPlan: string; submitLoginPlan: string;
demoHintDefault: string; demoHintDefault: string;
@ -55,6 +71,14 @@ function destLabel(path: string, t: LoginDict): string {
if (path === "/compare") return t.destCompare; if (path === "/compare") return t.destCompare;
if (path === "/tools") return t.destTools; if (path === "/tools") return t.destTools;
if (path.startsWith("/destinations/")) return t.destDestination; if (path.startsWith("/destinations/")) return t.destDestination;
if (path.startsWith("/dating")) return t.destDating;
if (path.startsWith("/chat")) return t.destChat;
if (path.startsWith("/meetups")) return t.destMeetups;
if (path.startsWith("/join")) return t.destJoin;
if (path.startsWith("/notifications")) return t.destNotifications;
if (path.startsWith("/gigs")) return t.destGigs;
if (path.startsWith("/community")) return t.destCommunity;
if (path === "/submit") return t.destSubmit;
return t.destProfile; return t.destProfile;
} }
@ -64,6 +88,14 @@ function hintFor(path: string, t: LoginDict): string {
if (path === "/tools") return t.hintTools; if (path === "/tools") return t.hintTools;
if (path.startsWith("/destinations/")) return t.hintDestination; if (path.startsWith("/destinations/")) return t.hintDestination;
if (path === "/profile") return t.hintProfile; if (path === "/profile") return t.hintProfile;
if (path.startsWith("/dating")) return t.hintDating;
if (path.startsWith("/chat")) return t.hintChat;
if (path.startsWith("/meetups")) return t.hintMeetups;
if (path.startsWith("/join")) return t.hintJoin;
if (path.startsWith("/notifications")) return t.hintNotifications;
if (path.startsWith("/gigs")) return t.hintGigs;
if (path.startsWith("/community")) return t.hintCommunity;
if (path === "/submit") return t.hintSubmit;
return t.hintExplore; return t.hintExplore;
} }

View File

@ -249,6 +249,14 @@ export const zh = {
hintCompare: "登录后继续你的城市对比", hintCompare: "登录后继续你的城市对比",
hintTools: "登录后使用需要账号的工具与服务", hintTools: "登录后使用需要账号的工具与服务",
hintDestination: "登录后回到该城市页面,收藏与计划自动同步", hintDestination: "登录后回到该城市页面,收藏与计划自动同步",
hintDating: "登录后开始滑动匹配,成功可私信聊天",
hintChat: "登录后查看与回复私信",
hintMeetups: "登录后报名活动或发起聚会",
hintJoin: "登录后开通或管理 VIP 会员",
hintNotifications: "登录后查看通知与提醒",
hintGigs: "登录后申请赏金或发布任务",
hintCommunity: "登录后发帖与参与讨论",
hintSubmit: "登录后提交内容投稿",
redirectTo: "登录后将前往", redirectTo: "登录后将前往",
planMergeNote: "检测到本机已有行程,登录后会自动与账号合并", planMergeNote: "检测到本机已有行程,登录后会自动与账号合并",
destHome: "首页", destHome: "首页",
@ -257,6 +265,14 @@ export const zh = {
destCompare: "城市对比", destCompare: "城市对比",
destTools: "工具箱", destTools: "工具箱",
destDestination: "城市详情", destDestination: "城市详情",
destDating: "游民匹配",
destChat: "私信",
destMeetups: "游民活动",
destJoin: "会员中心",
destNotifications: "通知中心",
destGigs: "赏金任务",
destCommunity: "社区讨论",
destSubmit: "内容投稿",
tabLogin: "登录", tabLogin: "登录",
tabRegister: "注册", tabRegister: "注册",
nickname: "昵称", nickname: "昵称",
@ -341,6 +357,7 @@ export const zh = {
tag: "✉️ MESSAGES", tag: "✉️ MESSAGES",
title: "私信", title: "私信",
loginTitle: "登录后查看私信", loginTitle: "登录后查看私信",
loginDesc: "匹配成功或活动结识的游民,在这里继续对话",
empty: "还没有对话,去匹配页滑一张卡片吧", empty: "还没有对话,去匹配页滑一张卡片吧",
noMessages: "暂无消息", noMessages: "暂无消息",
back: "返回列表", back: "返回列表",
@ -476,6 +493,7 @@ export const zh = {
hostSubmit: "发布活动", hostSubmit: "发布活动",
hostOk: "活动已发布", hostOk: "活动已发布",
hostFail: "发布失败", hostFail: "发布失败",
loginDesc: "登录后即可创建线上或线下游民活动",
empty: "暂无匹配的活动", empty: "暂无匹配的活动",
}, },
community: { community: {
@ -889,6 +907,14 @@ export const en: { [K in keyof typeof zh]: typeof zh[K] extends string ? string
hintCompare: "Continue your city comparison after sign-in", hintCompare: "Continue your city comparison after sign-in",
hintTools: "Access tools and services that need an account", hintTools: "Access tools and services that need an account",
hintDestination: "Return to this city with favorites and plans in sync", hintDestination: "Return to this city with favorites and plans in sync",
hintDating: "Sign in to swipe matches and chat on mutual like",
hintChat: "Sign in to read and reply to messages",
hintMeetups: "Sign in to RSVP or host nomad events",
hintJoin: "Sign in to manage or upgrade VIP membership",
hintNotifications: "Sign in to view alerts and reminders",
hintGigs: "Sign in to apply for gigs or post bounties",
hintCommunity: "Sign in to post and join discussions",
hintSubmit: "Sign in to submit content",
redirectTo: "After sign-in you'll go to", redirectTo: "After sign-in you'll go to",
planMergeNote: "Local trip data found — we'll merge it with your account", planMergeNote: "Local trip data found — we'll merge it with your account",
destHome: "Home", destHome: "Home",
@ -897,6 +923,14 @@ export const en: { [K in keyof typeof zh]: typeof zh[K] extends string ? string
destCompare: "Compare", destCompare: "Compare",
destTools: "Toolkit", destTools: "Toolkit",
destDestination: "City page", destDestination: "City page",
destDating: "Nomad match",
destChat: "Messages",
destMeetups: "Events",
destJoin: "Membership",
destNotifications: "Notifications",
destGigs: "Bounty gigs",
destCommunity: "Community",
destSubmit: "Content submit",
tabLogin: "Log in", tabLogin: "Log in",
tabRegister: "Sign up", tabRegister: "Sign up",
nickname: "Nickname", nickname: "Nickname",
@ -981,6 +1015,7 @@ export const en: { [K in keyof typeof zh]: typeof zh[K] extends string ? string
tag: "✉️ MESSAGES", tag: "✉️ MESSAGES",
title: "Messages", title: "Messages",
loginTitle: "Sign in to view messages", loginTitle: "Sign in to view messages",
loginDesc: "Continue conversations with nomads you matched or met at events",
empty: "No conversations yet — swipe on the match page", empty: "No conversations yet — swipe on the match page",
noMessages: "No messages yet", noMessages: "No messages yet",
back: "Back to inbox", back: "Back to inbox",
@ -1116,6 +1151,7 @@ export const en: { [K in keyof typeof zh]: typeof zh[K] extends string ? string
hostSubmit: "Publish", hostSubmit: "Publish",
hostOk: "Event published", hostOk: "Event published",
hostFail: "Could not publish", hostFail: "Could not publish",
loginDesc: "Sign in to host online or in-person nomad events",
empty: "No events match this filter", empty: "No events match this filter",
}, },
community: { community: {