diff --git a/frontend/src/app/ai/page.tsx b/frontend/src/app/ai/page.tsx
index 400f053..688cafb 100644
--- a/frontend/src/app/ai/page.tsx
+++ b/frontend/src/app/ai/page.tsx
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
+import { api } from "@/lib/api";
import SiteShell from "@/components/SiteShell";
import AiAssistantClient from "@/components/AiAssistantClient";
@@ -7,10 +8,13 @@ export const metadata: Metadata = {
description: "智能推荐下一站游民城市",
};
-export default function AiPage() {
+export const revalidate = 60;
+
+export default async function AiPage() {
+ const destinations = await api.getDestinations().catch(() => []);
return (
-
+
);
}
diff --git a/frontend/src/app/changelog/page.tsx b/frontend/src/app/changelog/page.tsx
index c8bf6b7..5f3c1c4 100644
--- a/frontend/src/app/changelog/page.tsx
+++ b/frontend/src/app/changelog/page.tsx
@@ -8,6 +8,15 @@ export const metadata: Metadata = {
};
const LOGS = [
+ {
+ date: "2026-08-30",
+ tag: "三环登录与导航",
+ items: [
+ "Connect:个人中心三环快捷入口、匹配加载态、发起活动/发赏金登录门、私信与登录提示",
+ "Explore:全站快捷键、首页 hash 锚点、对比空态、AI 推荐真正写入计划",
+ "Grow:导航补全服务/投稿、表单校验、学院空态与 VIP 课时直达开通",
+ ],
+ },
{
date: "2026-08-30",
tag: "三环可用深化",
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index ca38737..e9f851e 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -10903,6 +10903,14 @@ img { max-width: 100%; display: block; }
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 {
font-size: 0.8rem;
color: var(--text-muted);
diff --git a/frontend/src/app/profile/page.tsx b/frontend/src/app/profile/page.tsx
index a1af31e..59af3ba 100644
--- a/frontend/src/app/profile/page.tsx
+++ b/frontend/src/app/profile/page.tsx
@@ -73,7 +73,7 @@ export default function ProfilePage() {
useEffect(() => {
if (!token) {
- router.push("/login");
+ router.push("/login?next=/profile");
return;
}
setLoading(true);
@@ -291,8 +291,13 @@ export default function ProfilePage() {
🌍 浏览目的地
🗓️ 旅居计划
⚖️ 城市对比
- 📋 签证指南
- 📝 阅读博客
+ 🧭 智能下一站
+ 🎉 游民活动
+ 💕 游民匹配
+ ✉️ 私信
+ 🔔 通知
+ {vip ? "✨ VIP 会员" : "✨ 开通 VIP"}
+ 🎓 游民学院
🛠️ 工具箱
diff --git a/frontend/src/components/AiAssistantClient.tsx b/frontend/src/components/AiAssistantClient.tsx
index 6a74e77..3b1fe7a 100644
--- a/frontend/src/components/AiAssistantClient.tsx
+++ b/frontend/src/components/AiAssistantClient.tsx
@@ -2,9 +2,12 @@
import { useState } from "react";
import Link from "next/link";
+import { useRouter } from "next/navigation";
import { api } from "@/lib/api";
import { useI18n } from "@/lib/i18n";
import { useToast } from "@/lib/toast";
+import { mergeDestinationsIntoTrip } from "@/lib/tripActions";
+import type { Destination } from "@/lib/types";
import RingNext from "@/components/RingNext";
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 { toast } = useToast();
+ const router = useRouter();
const rings = useRingSteps();
const [message, setMessage] = 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 (
@@ -93,9 +119,11 @@ export default function AiAssistantClient() {
城市对比
-
- 写入计划
-
+ {cities.length > 0 && (
+
+ )}
)}
diff --git a/frontend/src/components/ChatClient.tsx b/frontend/src/components/ChatClient.tsx
index fecc697..0f2dcfb 100644
--- a/frontend/src/components/ChatClient.tsx
+++ b/frontend/src/components/ChatClient.tsx
@@ -45,6 +45,7 @@ export default function ChatClient() {
{t.chat.loginTitle}
+
{t.chat.loginDesc}
{t.nav.login}
diff --git a/frontend/src/components/ChatThreadClient.tsx b/frontend/src/components/ChatThreadClient.tsx
index 6262ae7..7c4cab3 100644
--- a/frontend/src/components/ChatThreadClient.tsx
+++ b/frontend/src/components/ChatThreadClient.tsx
@@ -62,6 +62,7 @@ export default function ChatThreadClient({ convId }: { convId: string }) {
{t.chat.loginTitle}
+
{t.chat.loginDesc}
{t.nav.login}
diff --git a/frontend/src/components/CommunityHubClient.tsx b/frontend/src/components/CommunityHubClient.tsx
index 3025977..52e0210 100644
--- a/frontend/src/components/CommunityHubClient.tsx
+++ b/frontend/src/components/CommunityHubClient.tsx
@@ -125,9 +125,23 @@ export default function CommunityHubClient({ discussions, featured }: Props) {
{filtered.length === 0 && (
{t.community.empty}
-
- {t.community.newTopic}
-
+
+ {(category !== "全部" || q.trim()) && (
+
+ )}
+
+ {t.community.newTopic}
+
+
)}
diff --git a/frontend/src/components/CompareClient.tsx b/frontend/src/components/CompareClient.tsx
index 65dcce7..7bad7f6 100644
--- a/frontend/src/components/CompareClient.tsx
+++ b/frontend/src/components/CompareClient.tsx
@@ -145,7 +145,11 @@ export default function CompareClient({ destinations }: Props) {
⚖️
{t.compare.needMore} {Math.max(0, 2 - selected.length)} {t.compare.needMoreSuffix}
-
{t.compare.backDest}
+
+ {t.compare.backDest}
+ {t.nav.nextStop}
+ {t.nav.ai}
+
) : (
<>
diff --git a/frontend/src/components/ContentSubmitClient.tsx b/frontend/src/components/ContentSubmitClient.tsx
index 094e0bc..522c366 100644
--- a/frontend/src/components/ContentSubmitClient.tsx
+++ b/frontend/src/components/ContentSubmitClient.tsx
@@ -8,11 +8,15 @@ import { useAuth } from "@/lib/auth";
import { useToast } from "@/lib/toast";
import { useI18n } from "@/lib/i18n";
+import RingNext from "@/components/RingNext";
+import { useRingSteps } from "@/lib/rings";
+
export default function ContentSubmitClient() {
const { token } = useAuth();
const router = useRouter();
const { toast } = useToast();
const { t } = useI18n();
+ const rings = useRingSteps();
const [title, setTitle] = useState("");
const [url, setUrl] = useState("");
const [notes, setNotes] = useState("");
@@ -29,6 +33,10 @@ export default function ContentSubmitClient() {
toast("请填写更完整的标题", "info");
return;
}
+ if ((contentType === "article" || contentType === "video") && !/^https?:\/\/.+/.test(url.trim())) {
+ toast("文章/视频请填写有效链接(http/https)", "info");
+ return;
+ }
setBusy(true);
try {
await api.submitContent(
@@ -113,6 +121,7 @@ export default function ContentSubmitClient() {
)}
)}
+
);
diff --git a/frontend/src/components/DatingClient.tsx b/frontend/src/components/DatingClient.tsx
index 4b8bf57..4567162 100644
--- a/frontend/src/components/DatingClient.tsx
+++ b/frontend/src/components/DatingClient.tsx
@@ -160,7 +160,9 @@ export default function DatingClient() {
)}
- {loading && joined && 加载候选人…
}
+ {loading && (
+ {joined ? "加载候选人…" : "加载匹配资料…"}
+ )}
{top && (
diff --git a/frontend/src/components/DigitalCourseClient.tsx b/frontend/src/components/DigitalCourseClient.tsx
index ac601ed..953eb38 100644
--- a/frontend/src/components/DigitalCourseClient.tsx
+++ b/frontend/src/components/DigitalCourseClient.tsx
@@ -39,7 +39,14 @@ export default function DigitalCourseClient({ course }: { course: DigitalCourse
)}
{course.modules.length === 0 ? (
- 课程内容准备中
+
+
课程内容准备中
+
+ {t.digital.back}
+ {t.nav.gigs}
+ {t.nav.join}
+
+
) : (
{course.modules.map((mod, mi) => (
@@ -50,16 +57,22 @@ export default function DigitalCourseClient({ course }: { course: DigitalCourse
const locked = !lesson.free && !vip;
return (
-
-
- {locked ? "🔒 " : ""}
- {lesson.title}
-
-
- {lesson.duration}
- {lesson.free ? ` · ${t.digital.free}` : " · VIP"}
-
-
+ {locked ? (
+
+ 🔒 {lesson.title}
+
+ {lesson.duration} · VIP · 开通解锁
+
+
+ ) : (
+
+ {lesson.title}
+
+ {lesson.duration}
+ {lesson.free ? ` · ${t.digital.free}` : " · VIP"}
+
+
+ )}
);
})}
diff --git a/frontend/src/components/FeedbackClient.tsx b/frontend/src/components/FeedbackClient.tsx
index 9deff62..dc18d85 100644
--- a/frontend/src/components/FeedbackClient.tsx
+++ b/frontend/src/components/FeedbackClient.tsx
@@ -6,11 +6,14 @@ import { api } from "@/lib/api";
import { useAuth } from "@/lib/auth";
import { useToast } from "@/lib/toast";
import { useI18n } from "@/lib/i18n";
+import RingNext from "@/components/RingNext";
+import { useRingSteps } from "@/lib/rings";
export default function FeedbackClient() {
const { token } = useAuth();
const { toast } = useToast();
const { t } = useI18n();
+ const rings = useRingSteps();
const [content, setContent] = useState("");
const [category, setCategory] = useState("general");
const [loading, setLoading] = useState(false);
@@ -79,6 +82,7 @@ export default function FeedbackClient() {
{!token &&
未登录也可提交;登录后便于我们回复你
}
)}
+
);
diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx
index a2201fa..30b3711 100644
--- a/frontend/src/components/Footer.tsx
+++ b/frontend/src/components/Footer.tsx
@@ -61,6 +61,9 @@ export default function Footer() {
{t.footer.grow}
{t.footer.digital}
{t.footer.gigs}
+ {t.nav.services}
+ {t.submit.tag}
+ {t.nav.feedback}
{t.footer.toolkit}
{t.footer.join}
diff --git a/frontend/src/components/GigPostClient.tsx b/frontend/src/components/GigPostClient.tsx
index 559777d..3aa91e0 100644
--- a/frontend/src/components/GigPostClient.tsx
+++ b/frontend/src/components/GigPostClient.tsx
@@ -19,6 +19,21 @@ export default function GigPostClient() {
const [deadline, setDeadline] = useState("");
const [busy, setBusy] = useState(false);
+ if (!token) {
+ return (
+
+
+
+
+
{t.gigs.postTitle}
+
{t.gigs.loginFirst}
+
{t.nav.login}
+
+
+
+ );
+ }
+
const submit = async () => {
if (!token) {
router.push("/login?next=/gigs/post");
diff --git a/frontend/src/components/HomeClient.tsx b/frontend/src/components/HomeClient.tsx
index 298d254..23f5837 100644
--- a/frontend/src/components/HomeClient.tsx
+++ b/frontend/src/components/HomeClient.tsx
@@ -53,6 +53,17 @@ export default function HomeClient(props: Props) {
const [chromeReady, setChromeReady] = useState(false);
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(() => {
const onMatcher = () => setMatcherOpen(true);
window.addEventListener("open-matcher", onMatcher);
diff --git a/frontend/src/components/MeetupLiveClient.tsx b/frontend/src/components/MeetupLiveClient.tsx
index 9ecc17b..c700756 100644
--- a/frontend/src/components/MeetupLiveClient.tsx
+++ b/frontend/src/components/MeetupLiveClient.tsx
@@ -122,7 +122,17 @@ export default function MeetupLiveClient({ meetupId, title }: { meetupId: string
)}
{!session.videoUrl && !session.chatUrl && (
- 直播链接尚未配置
+
+
直播链接尚未配置
+
+
+ {t.live.back}
+
+
+ {t.nav.feedback}
+
+
+
)}
diff --git a/frontend/src/components/MeetupsHostClient.tsx b/frontend/src/components/MeetupsHostClient.tsx
index 5fc0602..3848039 100644
--- a/frontend/src/components/MeetupsHostClient.tsx
+++ b/frontend/src/components/MeetupsHostClient.tsx
@@ -29,6 +29,21 @@ export default function MeetupsHostClient() {
const set = (k: string, v: string | number) => setForm((f) => ({ ...f, [k]: v }));
+ if (!token) {
+ return (
+
+
+
+
+
{t.meetups.hostTitle}
+
{t.meetups.loginDesc}
+
{t.nav.login}
+
+
+
+ );
+ }
+
const submit = async () => {
if (!token) {
router.push("/login?next=/meetups/host");
diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx
index 8f845c0..aa55e2b 100644
--- a/frontend/src/components/Navbar.tsx
+++ b/frontend/src/components/Navbar.tsx
@@ -58,11 +58,15 @@ export default function Navbar() {
{ href: "/community", label: t.nav.community, section: "community" },
{ href: "/dating", label: t.nav.dating, section: "dating" },
{ 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[] = [
{ href: "/digital", label: t.nav.digital, section: "digital" },
{ 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: "/book", label: t.nav.ebook, section: "ebook" },
];
diff --git a/frontend/src/components/ServicesClient.tsx b/frontend/src/components/ServicesClient.tsx
index 664b5c6..9bedba1 100644
--- a/frontend/src/components/ServicesClient.tsx
+++ b/frontend/src/components/ServicesClient.tsx
@@ -30,6 +30,10 @@ export default function ServicesClient({ services }: { services: ServiceItem[] }
toast("请填写姓名、邮箱和需求说明", "info");
return;
}
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(f.email.trim())) {
+ toast("请填写有效邮箱地址", "info");
+ return;
+ }
setBusyId(serviceId);
try {
await api.submitServiceLead(serviceId, {
diff --git a/frontend/src/components/SiteShell.tsx b/frontend/src/components/SiteShell.tsx
index cd85559..9e3b633 100644
--- a/frontend/src/components/SiteShell.tsx
+++ b/frontend/src/components/SiteShell.tsx
@@ -1,12 +1,19 @@
"use client";
import { useEffect } from "react";
+import dynamic from "next/dynamic";
+import { usePathname, useRouter } from "next/navigation";
import Navbar from "./Navbar";
import ParticleCanvas from "./ParticleCanvas";
import Footer from "./Footer";
import GlobalSearch from "./GlobalSearch";
+const KeyboardShortcuts = dynamic(() => import("./KeyboardShortcuts"), { ssr: false });
+
export default function SiteShell({ children, showFooter = false }: { children: React.ReactNode; showFooter?: boolean }) {
+ const pathname = usePathname();
+ const router = useRouter();
+
useEffect(() => {
const observer = new IntersectionObserver(
(entries) =>
@@ -35,6 +42,15 @@ export default function SiteShell({ children, showFooter = false }: { children:
+ {
+ if (pathname === "/") {
+ window.dispatchEvent(new Event("open-matcher"));
+ } else {
+ router.push("/next-stop");
+ }
+ }}
+ />
{children}
{showFooter && }
>
diff --git a/frontend/src/lib/authNext.ts b/frontend/src/lib/authNext.ts
index 65c0920..de5cb45 100644
--- a/frontend/src/lib/authNext.ts
+++ b/frontend/src/lib/authNext.ts
@@ -7,6 +7,14 @@ type LoginDict = {
hintCompare: string;
hintTools: string;
hintDestination: string;
+ hintDating: string;
+ hintChat: string;
+ hintMeetups: string;
+ hintJoin: string;
+ hintNotifications: string;
+ hintGigs: string;
+ hintCommunity: string;
+ hintSubmit: string;
redirectTo: string;
planMergeNote: string;
destHome: string;
@@ -15,6 +23,14 @@ type LoginDict = {
destCompare: string;
destTools: string;
destDestination: string;
+ destDating: string;
+ destChat: string;
+ destMeetups: string;
+ destJoin: string;
+ destNotifications: string;
+ destGigs: string;
+ destCommunity: string;
+ destSubmit: string;
submitLoginDefault: string;
submitLoginPlan: string;
demoHintDefault: string;
@@ -55,6 +71,14 @@ function destLabel(path: string, t: LoginDict): string {
if (path === "/compare") return t.destCompare;
if (path === "/tools") return t.destTools;
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;
}
@@ -64,6 +88,14 @@ function hintFor(path: string, t: LoginDict): string {
if (path === "/tools") return t.hintTools;
if (path.startsWith("/destinations/")) return t.hintDestination;
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;
}
diff --git a/frontend/src/lib/i18n/dictionaries.ts b/frontend/src/lib/i18n/dictionaries.ts
index d62b541..bb45548 100644
--- a/frontend/src/lib/i18n/dictionaries.ts
+++ b/frontend/src/lib/i18n/dictionaries.ts
@@ -249,6 +249,14 @@ export const zh = {
hintCompare: "登录后继续你的城市对比",
hintTools: "登录后使用需要账号的工具与服务",
hintDestination: "登录后回到该城市页面,收藏与计划自动同步",
+ hintDating: "登录后开始滑动匹配,成功可私信聊天",
+ hintChat: "登录后查看与回复私信",
+ hintMeetups: "登录后报名活动或发起聚会",
+ hintJoin: "登录后开通或管理 VIP 会员",
+ hintNotifications: "登录后查看通知与提醒",
+ hintGigs: "登录后申请赏金或发布任务",
+ hintCommunity: "登录后发帖与参与讨论",
+ hintSubmit: "登录后提交内容投稿",
redirectTo: "登录后将前往",
planMergeNote: "检测到本机已有行程,登录后会自动与账号合并",
destHome: "首页",
@@ -257,6 +265,14 @@ export const zh = {
destCompare: "城市对比",
destTools: "工具箱",
destDestination: "城市详情",
+ destDating: "游民匹配",
+ destChat: "私信",
+ destMeetups: "游民活动",
+ destJoin: "会员中心",
+ destNotifications: "通知中心",
+ destGigs: "赏金任务",
+ destCommunity: "社区讨论",
+ destSubmit: "内容投稿",
tabLogin: "登录",
tabRegister: "注册",
nickname: "昵称",
@@ -341,6 +357,7 @@ export const zh = {
tag: "✉️ MESSAGES",
title: "私信",
loginTitle: "登录后查看私信",
+ loginDesc: "匹配成功或活动结识的游民,在这里继续对话",
empty: "还没有对话,去匹配页滑一张卡片吧",
noMessages: "暂无消息",
back: "返回列表",
@@ -476,6 +493,7 @@ export const zh = {
hostSubmit: "发布活动",
hostOk: "活动已发布",
hostFail: "发布失败",
+ loginDesc: "登录后即可创建线上或线下游民活动",
empty: "暂无匹配的活动",
},
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",
hintTools: "Access tools and services that need an account",
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",
planMergeNote: "Local trip data found — we'll merge it with your account",
destHome: "Home",
@@ -897,6 +923,14 @@ export const en: { [K in keyof typeof zh]: typeof zh[K] extends string ? string
destCompare: "Compare",
destTools: "Toolkit",
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",
tabRegister: "Sign up",
nickname: "Nickname",
@@ -981,6 +1015,7 @@ export const en: { [K in keyof typeof zh]: typeof zh[K] extends string ? string
tag: "✉️ MESSAGES",
title: "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",
noMessages: "No messages yet",
back: "Back to inbox",
@@ -1116,6 +1151,7 @@ export const en: { [K in keyof typeof zh]: typeof zh[K] extends string ? string
hostSubmit: "Publish",
hostOk: "Event published",
hostFail: "Could not publish",
+ loginDesc: "Sign in to host online or in-person nomad events",
empty: "No events match this filter",
},
community: {