Polish edge UX: 404/error rings, sitemap, matcher empty, shortcuts and tips.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
dac99e8ef4
commit
2ee0f3bf77
@ -16,6 +16,7 @@ const LOGS = [
|
||||
"Connect:RSVP 需登录、通知铃铛、匹配配额、私信与 VIP 支付加固",
|
||||
"Grow:学院 VIP 课时客户端解锁、赏金/投稿表单校验、电子书购买闭环",
|
||||
"Explore:目的地对比入口、AI 快捷提问、计划空态、周报订阅诚实提示",
|
||||
"边缘体验:404/错误页环导航、sitemap 补全、匹配空态、快捷键与贴士持久化",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
40
frontend/src/app/error.tsx
Normal file
40
frontend/src/app/error.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import SiteShell from "@/components/SiteShell";
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<SiteShell showFooter>
|
||||
<div className="not-found-page">
|
||||
<h1 className="not-found-title" style={{ fontSize: "2.5rem" }}>
|
||||
出错了
|
||||
</h1>
|
||||
<p>页面暂时无法加载,可以重试或换一条航线</p>
|
||||
<p className="not-found-sub">{error.digest ? `参考码 ${error.digest}` : "请稍后再试"}</p>
|
||||
<div className="not-found-actions">
|
||||
<button type="button" className="btn btn-primary" onClick={reset}>
|
||||
重试
|
||||
</button>
|
||||
<Link href="/" className="btn">
|
||||
首页
|
||||
</Link>
|
||||
<Link href="/feedback" className="btn btn-ghost">
|
||||
反馈问题
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
@ -6082,6 +6082,18 @@ img { max-width: 100%; display: block; }
|
||||
animation: toastIn 0.3s ease;
|
||||
}
|
||||
|
||||
.nomad-tips-link {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--accent-3);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nomad-tips-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.nomad-tips-close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
|
||||
@ -3,7 +3,7 @@ import SiteShell from "@/components/SiteShell";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<SiteShell>
|
||||
<SiteShell showFooter>
|
||||
<div className="not-found-page">
|
||||
<div className="not-found-compass" aria-hidden="true">
|
||||
<span className="compass-ring">🧭</span>
|
||||
@ -11,10 +11,23 @@ export default function NotFound() {
|
||||
</div>
|
||||
<h1 className="not-found-title">404</h1>
|
||||
<p>哎呀,这片海域没有标记的岛屿</p>
|
||||
<p className="not-found-sub">你可能走错了航线,让我们帮你回到正轨</p>
|
||||
<p className="not-found-sub">你可能走错了航线,从三条产品环任选一条继续</p>
|
||||
<div className="not-found-actions">
|
||||
<Link href="/" className="btn btn-primary">返回首页 🏠</Link>
|
||||
<Link href="/#destinations" className="btn btn-ghost">探索目的地 🌍</Link>
|
||||
<Link href="/" className="btn btn-primary">
|
||||
返回首页
|
||||
</Link>
|
||||
<Link href="/next-stop" className="btn">
|
||||
智能下一站
|
||||
</Link>
|
||||
<Link href="/meetups" className="btn">
|
||||
游民活动
|
||||
</Link>
|
||||
<Link href="/digital" className="btn">
|
||||
游民学院
|
||||
</Link>
|
||||
<Link href="/help" className="btn btn-ghost">
|
||||
帮助中心
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</SiteShell>
|
||||
|
||||
@ -35,11 +35,20 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||
{ url: `${SITE}/contact`, lastModified: now, changeFrequency: "yearly", priority: 0.4 },
|
||||
{ url: `${SITE}/help`, lastModified: now, changeFrequency: "yearly", priority: 0.4 },
|
||||
{ url: `${SITE}/terms`, lastModified: now, changeFrequency: "yearly", priority: 0.3 },
|
||||
{ url: `${SITE}/join`, lastModified: now, changeFrequency: "monthly", priority: 0.75 },
|
||||
{ url: `${SITE}/digital`, lastModified: now, changeFrequency: "weekly", priority: 0.85 },
|
||||
{ url: `${SITE}/digital/course`, lastModified: now, changeFrequency: "weekly", priority: 0.8 },
|
||||
{ url: `${SITE}/digital/jobs`, lastModified: now, changeFrequency: "weekly", priority: 0.75 },
|
||||
{ url: `${SITE}/digital/day/1`, lastModified: now, changeFrequency: "monthly", priority: 0.55 },
|
||||
{ url: `${SITE}/digital/day/2`, lastModified: now, changeFrequency: "monthly", priority: 0.55 },
|
||||
{ url: `${SITE}/digital/day/3`, lastModified: now, changeFrequency: "monthly", priority: 0.55 },
|
||||
{ url: `${SITE}/book`, lastModified: now, changeFrequency: "weekly", priority: 0.85 },
|
||||
{ url: `${SITE}/book/read`, lastModified: now, changeFrequency: "weekly", priority: 0.8 },
|
||||
{ url: `${SITE}/support`, lastModified: now, changeFrequency: "monthly", priority: 0.5 },
|
||||
{ url: `${SITE}/careers`, lastModified: now, changeFrequency: "monthly", priority: 0.45 },
|
||||
{ url: `${SITE}/safety`, lastModified: now, changeFrequency: "yearly", priority: 0.4 },
|
||||
{ url: `${SITE}/guidelines`, lastModified: now, changeFrequency: "yearly", priority: 0.4 },
|
||||
{ url: `${SITE}/cookies`, lastModified: now, changeFrequency: "yearly", priority: 0.3 },
|
||||
{ url: `${SITE}/press`, lastModified: now, changeFrequency: "yearly", priority: 0.35 },
|
||||
{ url: `${SITE}/login`, lastModified: now, changeFrequency: "monthly", priority: 0.5 },
|
||||
{ url: `${SITE}/privacy`, lastModified: now, changeFrequency: "yearly", priority: 0.3 },
|
||||
{ url: `${SITE}/about`, lastModified: now, changeFrequency: "monthly", priority: 0.5 },
|
||||
|
||||
@ -215,8 +215,22 @@ export default function DestinationMatcher({ destinations, open, onClose }: Prop
|
||||
<h2>为你推荐的目的地</h2>
|
||||
<p>偏好已保存。可一键写入旅居计划,或先对比再决定。</p>
|
||||
</div>
|
||||
{results.length === 0 ? (
|
||||
<div className="notif-empty">
|
||||
<p className="dest-empty">暂无匹配结果,目的地数据可能未加载</p>
|
||||
<div className="dating-empty-actions">
|
||||
<button type="button" className="btn btn-ghost" onClick={reset}>
|
||||
重新测试
|
||||
</button>
|
||||
<Link href="/next-stop" className="btn btn-primary" onClick={onClose}>
|
||||
去智能下一站
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="matcher-result-list">
|
||||
{results.map(({ dest, score }, i) => (
|
||||
{results.slice(0, 8).map(({ dest, score }, i) => (
|
||||
<div key={dest.slug} className={`matcher-result-card${i === 0 ? " top" : ""}`}>
|
||||
<Link href={`/destinations/${dest.slug}`} className="matcher-result-main" onClick={onClose}>
|
||||
<div className="matcher-result-rank">
|
||||
@ -246,7 +260,7 @@ export default function DestinationMatcher({ destinations, open, onClose }: Prop
|
||||
))}
|
||||
</div>
|
||||
<div className="matcher-actions">
|
||||
<button className="btn btn-ghost" onClick={reset}>🔄 重新测试</button>
|
||||
<button type="button" className="btn btn-ghost" onClick={reset}>🔄 重新测试</button>
|
||||
{top3.length >= 2 && (
|
||||
<Link
|
||||
href={compareUrl(top3.map((d) => d.slug))}
|
||||
@ -256,10 +270,12 @@ export default function DestinationMatcher({ destinations, open, onClose }: Prop
|
||||
⚖️ 对比 Top {top3.length}
|
||||
</Link>
|
||||
)}
|
||||
<button className="btn btn-primary" onClick={buildPlanFromTop} disabled={top3.length === 0}>
|
||||
<button type="button" className="btn btn-primary" onClick={buildPlanFromTop} disabled={top3.length === 0}>
|
||||
把 Top {Math.min(3, top3.length)} 写入计划 →
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -62,6 +62,23 @@ export default function FAQSection({ faqs, limit }: Props) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<div className="notif-empty reveal">
|
||||
<p className="dest-empty">{locale === "en" ? "No FAQ matched" : "没有匹配的问题"}</p>
|
||||
<div className="dating-empty-actions">
|
||||
<button type="button" className="btn btn-sm" onClick={() => setSearch("")}>
|
||||
{locale === "en" ? "Clear search" : "清除搜索"}
|
||||
</button>
|
||||
<a href="/help" className="btn btn-sm btn-ghost">
|
||||
{locale === "en" ? "Help center" : "帮助中心"}
|
||||
</a>
|
||||
<a href="/ai" className="btn btn-sm btn-ghost">
|
||||
{locale === "en" ? "Ask AI" : "问 AI"}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{limit && !showAll && sorted.length > limit && (
|
||||
<div className="dest-expand-wrap reveal">
|
||||
<button type="button" className="btn btn-ghost" onClick={() => setShowAll(true)}>
|
||||
@ -73,6 +90,10 @@ export default function FAQSection({ faqs, limit }: Props) {
|
||||
<p className="faq-plan-hint reveal">
|
||||
{locale === "en" ? "Still exploring? " : "还在选城?"}
|
||||
<a href="#destinations">{locale === "en" ? "Browse destinations" : "继续看目的地"}</a>
|
||||
{" · "}
|
||||
<a href="/help">{locale === "en" ? "Help center" : "帮助中心"}</a>
|
||||
{" · "}
|
||||
<a href="/ai">{locale === "en" ? "Ask AI" : "问 AI"}</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface Props {
|
||||
onOpenMatcher?: () => void;
|
||||
@ -9,21 +10,21 @@ interface Props {
|
||||
const SHORTCUTS = [
|
||||
{ keys: ["Ctrl", "K"], label: "全局搜索", icon: "🔍" },
|
||||
{ keys: ["M"], label: "智能匹配", icon: "🎯" },
|
||||
{ keys: ["G"], label: "命运转盘", icon: "🎲" },
|
||||
{ keys: ["N"], label: "下一站", icon: "🧭" },
|
||||
{ keys: ["P"], label: "旅居计划", icon: "🗓️" },
|
||||
{ keys: ["T"], label: "打开工具箱", icon: "🛠️" },
|
||||
{ keys: ["?"], label: "快捷键帮助", icon: "⌨️" },
|
||||
{ keys: ["Esc"], label: "关闭弹窗", icon: "✕" },
|
||||
{ keys: ["↑", "↓"], label: "搜索导航", icon: "↕️" },
|
||||
{ keys: ["Enter"], label: "确认选择", icon: "↵" },
|
||||
];
|
||||
|
||||
export default function KeyboardShortcuts({ onOpenMatcher }: Props) {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
const tag = (e.target as HTMLElement)?.tagName;
|
||||
const typing = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
|
||||
const typing = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || (e.target as HTMLElement)?.isContentEditable;
|
||||
|
||||
if (e.key === "?" && !typing && !e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
@ -33,37 +34,50 @@ export default function KeyboardShortcuts({ onOpenMatcher }: Props) {
|
||||
e.preventDefault();
|
||||
onOpenMatcher?.();
|
||||
}
|
||||
if (e.key === "g" && !typing && !e.metaKey && !e.ctrlKey) {
|
||||
if (e.key === "n" && !typing && !e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
document.getElementById("spin")?.scrollIntoView({ behavior: "smooth" });
|
||||
router.push("/next-stop");
|
||||
}
|
||||
if (e.key === "p" && !typing && !e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
router.push("/plan");
|
||||
}
|
||||
if (e.key === "t" && !typing && !e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
window.location.href = "/tools";
|
||||
router.push("/tools");
|
||||
}
|
||||
if (e.key === "Escape" && open) setOpen(false);
|
||||
if (e.key === "Escape") setOpen(false);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [open, onOpenMatcher]);
|
||||
}, [onOpenMatcher, router]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-overlay open shortcuts-overlay" onClick={(e) => e.target === e.currentTarget && setOpen(false)}>
|
||||
<div className="modal shortcuts-modal">
|
||||
<button className="modal-close" onClick={() => setOpen(false)} aria-label="关闭">✕</button>
|
||||
<div
|
||||
className="modal-overlay open shortcuts-overlay"
|
||||
onClick={(e) => e.target === e.currentTarget && setOpen(false)}
|
||||
>
|
||||
<div className="modal shortcuts-modal" role="dialog" aria-label="键盘快捷键">
|
||||
<button type="button" className="modal-close" onClick={() => setOpen(false)} aria-label="关闭">
|
||||
✕
|
||||
</button>
|
||||
<div className="shortcuts-body">
|
||||
<span className="section-tag">⌨️ SHORTCUTS</span>
|
||||
<h2>键盘快捷键</h2>
|
||||
<p>提升浏览效率,按 <kbd>?</kbd> 随时唤起</p>
|
||||
<p>
|
||||
提升浏览效率,按 <kbd>?</kbd> 随时唤起
|
||||
</p>
|
||||
<div className="shortcuts-grid">
|
||||
{SHORTCUTS.map((s) => (
|
||||
<div key={s.label} className="shortcut-item">
|
||||
<span className="shortcut-icon">{s.icon}</span>
|
||||
<span className="shortcut-label">{s.label}</span>
|
||||
<div className="shortcut-keys">
|
||||
{s.keys.map((k) => <kbd key={k}>{k}</kbd>)}
|
||||
{s.keys.map((k) => (
|
||||
<kbd key={k}>{k}</kbd>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
const TIPS = [
|
||||
{ emoji: "📶", text: "入住前用 Speedtest 测网速,低于 20Mbps 慎重签约" },
|
||||
{ emoji: "💳", text: "准备一张无外汇手续费的信用卡,省下不少换汇成本" },
|
||||
{ emoji: "🏥", text: "出发前购买 SafetyWing 等国际医疗保险,月费约 $40+" },
|
||||
{ emoji: "🕐", text: "选时区重叠 ≥4 小时的城市,远程协作更轻松" },
|
||||
{ emoji: "🛂", text: "护照有效期建议预留 6 个月以上,避免登机被拒" },
|
||||
{ emoji: "☕", text: "先订 1 周 Airbnb 探路,再决定长期租房" },
|
||||
{ emoji: "📶", text: "入住前用 Speedtest 测网速,低于 20Mbps 慎重签约", href: "/digital/day/3" },
|
||||
{ emoji: "💳", text: "准备一张无外汇手续费的信用卡,省下不少换汇成本", href: "/digital/day/2" },
|
||||
{ emoji: "🏥", text: "出发前购买国际医疗保险,月费约 $40+", href: "/tools" },
|
||||
{ emoji: "🕐", text: "选时区重叠 ≥4 小时的城市,远程协作更轻松", href: "/next-stop" },
|
||||
{ emoji: "🛂", text: "护照有效期建议预留 6 个月以上,避免登机被拒", href: "/digital/day/1" },
|
||||
{ emoji: "☕", text: "先订 1 周短租探路,再决定长期租房", href: "/plan" },
|
||||
];
|
||||
|
||||
export default function NomadTips() {
|
||||
@ -16,6 +17,7 @@ export default function NomadTips() {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem("nomadro-tips-dismissed") === "1") return;
|
||||
const show = setTimeout(() => setVisible(true), 8000);
|
||||
return () => clearTimeout(show);
|
||||
}, []);
|
||||
@ -29,14 +31,23 @@ export default function NomadTips() {
|
||||
if (!visible) return null;
|
||||
|
||||
const tip = TIPS[idx];
|
||||
const dismiss = () => {
|
||||
localStorage.setItem("nomadro-tips-dismissed", "1");
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="nomad-tips" role="status">
|
||||
<button className="nomad-tips-close" onClick={() => setVisible(false)} aria-label="关闭">✕</button>
|
||||
<button type="button" className="nomad-tips-close" onClick={dismiss} aria-label="关闭">
|
||||
✕
|
||||
</button>
|
||||
<span className="nomad-tips-emoji">{tip.emoji}</span>
|
||||
<div className="nomad-tips-body">
|
||||
<strong>游民小贴士</strong>
|
||||
<p key={idx}>{tip.text}</p>
|
||||
<Link href={tip.href} className="nomad-tips-link" onClick={dismiss}>
|
||||
了解更多 →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="nomad-tips-dots">
|
||||
{TIPS.map((_, i) => (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useToast } from "@/lib/toast";
|
||||
|
||||
interface Props {
|
||||
@ -16,16 +16,33 @@ export default function ShareButton({ title, url, className = "", compact = fals
|
||||
|
||||
const getUrl = () => url ?? (typeof window !== "undefined" ? window.location.href : "");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setOpen(false);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [open]);
|
||||
|
||||
const copyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(getUrl());
|
||||
toast("链接已复制到剪贴板 📋");
|
||||
} catch {
|
||||
toast("复制失败,请手动复制地址栏", "error");
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const nativeShare = async () => {
|
||||
const shareUrl = getUrl();
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({ title, url: shareUrl });
|
||||
} catch {
|
||||
/* user cancelled */
|
||||
}
|
||||
setOpen(false);
|
||||
} else {
|
||||
await copyLink();
|
||||
@ -35,13 +52,13 @@ export default function ShareButton({ title, url, className = "", compact = fals
|
||||
const shareTwitter = () => {
|
||||
const shareUrl = encodeURIComponent(getUrl());
|
||||
const text = encodeURIComponent(title);
|
||||
window.open(`https://twitter.com/intent/tweet?text=${text}&url=${shareUrl}`, "_blank");
|
||||
window.open(`https://twitter.com/intent/tweet?text=${text}&url=${shareUrl}`, "_blank", "noopener,noreferrer");
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<button className={`share-btn-compact ${className}`} onClick={nativeShare} aria-label="分享">
|
||||
<button type="button" className={`share-btn-compact ${className}`} onClick={() => void nativeShare()} aria-label="分享">
|
||||
📤
|
||||
</button>
|
||||
);
|
||||
@ -49,16 +66,22 @@ export default function ShareButton({ title, url, className = "", compact = fals
|
||||
|
||||
return (
|
||||
<div className={`share-dropdown ${className}`}>
|
||||
<button className="btn btn-ghost share-trigger" onClick={() => setOpen(!open)}>
|
||||
<button type="button" className="btn btn-ghost share-trigger" onClick={() => setOpen(!open)} aria-expanded={open}>
|
||||
📤 分享
|
||||
</button>
|
||||
{open && (
|
||||
<>
|
||||
<div className="share-backdrop" onClick={() => setOpen(false)} />
|
||||
<div className="share-menu">
|
||||
<button onClick={nativeShare}>📱 系统分享</button>
|
||||
<button onClick={copyLink}>📋 复制链接</button>
|
||||
<button onClick={shareTwitter}>🐦 分享到 X</button>
|
||||
<div className="share-menu" role="menu">
|
||||
<button type="button" onClick={() => void nativeShare()}>
|
||||
📱 系统分享
|
||||
</button>
|
||||
<button type="button" onClick={() => void copyLink()}>
|
||||
📋 复制链接
|
||||
</button>
|
||||
<button type="button" onClick={shareTwitter}>
|
||||
🐦 分享到 X
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -80,6 +80,11 @@ export const STATIC_PAGES: Record<string, StaticPageDef> = {
|
||||
},
|
||||
{ heading: "违规处理", body: "内容可能被隐藏,严重者封禁账号。" },
|
||||
],
|
||||
cta: [
|
||||
{ label: "社区讨论", href: "/community" },
|
||||
{ label: "安全指南", href: "/safety" },
|
||||
{ label: "提交反馈", href: "/feedback" },
|
||||
],
|
||||
},
|
||||
terms: {
|
||||
tag: "📋 TERMS",
|
||||
@ -90,6 +95,10 @@ export const STATIC_PAGES: Record<string, StaticPageDef> = {
|
||||
{ heading: "用户责任", body: "你对自己发布的内容负责,并遵守当地法律法规。" },
|
||||
{ heading: "免责声明", body: "目的地数据、签证信息仅供参考,请以官方渠道为准。" },
|
||||
],
|
||||
cta: [
|
||||
{ label: "隐私政策", href: "/privacy" },
|
||||
{ label: "社区准则", href: "/guidelines" },
|
||||
],
|
||||
},
|
||||
cookies: {
|
||||
tag: "🍪 COOKIES",
|
||||
@ -109,6 +118,11 @@ export const STATIC_PAGES: Record<string, StaticPageDef> = {
|
||||
{ heading: "媒体联系", body: "press@nomadro.com" },
|
||||
{ heading: "数据亮点", body: "覆盖 195+ 国家目的地数据、52+ 旅居工具、活跃游民社区。" },
|
||||
],
|
||||
cta: [
|
||||
{ label: "关于我们", href: "/about" },
|
||||
{ label: "更新日志", href: "/changelog" },
|
||||
{ label: "联系合作", href: "/contact" },
|
||||
],
|
||||
},
|
||||
careers: {
|
||||
tag: "💼 CAREERS",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user