'grok建议功能'

This commit is contained in:
eric
2026-03-09 00:05:00 -05:00
parent 41487dc22e
commit 8eef28536e
14 changed files with 1044 additions and 87 deletions

81
app/[locale]/ai/page.tsx Normal file
View File

@@ -0,0 +1,81 @@
"use client";
import { useState } from "react";
import { useTranslation } from "@/i18n/navigation";
import Footer from "@/app/components/Footer";
export default function AIPage() {
const { t } = useTranslation("ai");
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false);
const exampleQueries = [
"我预算每月800刀想亚洲热带有稳定网适合freelance写作推荐城市+理由",
"帮我写下一站介绍文案",
"生成3月清迈行程计划",
];
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!query.trim()) return;
setLoading(true);
setTimeout(() => setLoading(false), 1500);
};
return (
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
<div className="max-w-2xl mx-auto px-4 sm:px-6 py-12 sm:py-16">
<div className="text-center mb-10">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100">
{t("title")}
</h1>
<p className="mt-2 text-gray-500 dark:text-gray-400">
{t("subtitle")}
</p>
</div>
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-6">
<form onSubmit={handleSubmit} className="space-y-4">
<textarea
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("placeholder")}
rows={4}
className="w-full rounded-xl border border-gray-200 dark:border-gray-700 px-4 py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400 resize-none focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20 focus:border-[#ff4d4f]"
/>
<button
type="submit"
disabled={loading || !query.trim()}
className="w-full py-3 rounded-xl bg-[#ff4d4f] text-white font-medium hover:bg-[#ff3333] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{loading ? t("thinking") : t("ask")}
</button>
</form>
<div className="mt-6 pt-6 border-t border-gray-100 dark:border-gray-800">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
{t("examples")}
</p>
<div className="space-y-2">
{exampleQueries.map((q, i) => (
<button
key={i}
type="button"
onClick={() => setQuery(q)}
className="block w-full text-left px-4 py-3 rounded-xl bg-gray-50 dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
>
{q}
</button>
))}
</div>
</div>
</div>
<p className="mt-6 text-center text-xs text-gray-400 dark:text-gray-500">
{t("comingSoon")}
</p>
</div>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,217 @@
"use client";
import { useState, useMemo } from "react";
import { useTranslation } from "@/i18n/navigation";
import { Link } from "@/i18n/navigation";
import { cities } from "@/app/data/cities";
import CityCard from "@/app/components/CityCard";
import CityModal from "@/app/components/CityModal";
import Footer from "@/app/components/Footer";
import type { City } from "@/app/data/cities";
const TAGS = ["远程工作者", "创业者", "设计师", "开发者", "内容创作者", "带宠物"];
const TIMEZONES = ["UTC+8 中国", "UTC+7 东南亚", "UTC+1 欧洲", "UTC-5 美东", "UTC-8 美西", "任意"];
const VISA_OPTIONS = ["免签优先", "落地签可", "需提前办", "任意"];
export default function DashboardPage() {
const { t } = useTranslation("dashboard");
const [budget, setBudget] = useState(5000);
const [internet, setInternet] = useState(50);
const [timezone, setTimezone] = useState("UTC+8 中国");
const [visa, setVisa] = useState("任意");
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [cityModal, setCityModal] = useState<City | null>(null);
const filteredCities = useMemo(() => {
return [...cities]
.filter((c) => c.costPerMonth <= budget * 1.2 && c.internetSpeed >= internet)
.sort((a, b) => b.overallScore - a.overallScore)
.slice(0, 12);
}, [budget, internet]);
const toggleTag = (tag: string) => {
setSelectedTags((prev) =>
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
);
};
return (
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12">
<div className="mb-8">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100">
{t("title")}
</h1>
<p className="mt-1 text-gray-500 dark:text-gray-400">
{t("subtitle")}
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<aside className="lg:col-span-1 space-y-6">
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 sm:p-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("budget")}
</label>
<input
type="range"
min="2000"
max="15000"
step="500"
value={budget}
onChange={(e) => setBudget(Number(e.target.value))}
className="w-full accent-[#ff4d4f]"
/>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
¥{budget.toLocaleString()}
</p>
</div>
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 sm:p-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("timezone")}
</label>
<select
value={timezone}
onChange={(e) => setTimezone(e.target.value)}
className="w-full rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
>
{TIMEZONES.map((z) => (
<option key={z} value={z}>{z}</option>
))}
</select>
</div>
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 sm:p-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("visa")}
</label>
<select
value={visa}
onChange={(e) => setVisa(e.target.value)}
className="w-full rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
>
{VISA_OPTIONS.map((v) => (
<option key={v} value={v}>{v}</option>
))}
</select>
</div>
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 sm:p-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("internet")}
</label>
<input
type="range"
min="30"
max="150"
step="10"
value={internet}
onChange={(e) => setInternet(Number(e.target.value))}
className="w-full accent-[#ff4d4f]"
/>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{internet} Mbps
</p>
</div>
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 sm:p-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
{t("tags")}
</label>
<div className="flex flex-wrap gap-2">
{TAGS.map((tag) => (
<button
key={tag}
onClick={() => toggleTag(tag)}
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${
selectedTags.includes(tag)
? "bg-[#ff4d4f] text-white"
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
}`}
>
{tag}
</button>
))}
</div>
</div>
<div className="rounded-2xl bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-950/30 dark:to-orange-950/30 border border-amber-200 dark:border-amber-800/50 p-4">
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">
🤖 {t("aiTip")}
</p>
<p className="mt-1 text-xs text-amber-700 dark:text-amber-300">
80% 3
</p>
</div>
</aside>
<main className="lg:col-span-2">
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 sm:p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
{t("search")}
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3" style={{ gridAutoRows: "160px" }}>
{filteredCities.map((city, i) => (
<div key={city.id} className="h-full min-h-[140px]">
<CityCard
city={city}
rank={i + 1}
onClick={() => setCityModal(city)}
/>
</div>
))}
</div>
{filteredCities.length === 0 && (
<p className="text-center py-8 text-gray-500 dark:text-gray-400">
{t("noMatch")}
</p>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 h-40 flex items-center justify-center">
<span className="text-gray-400 dark:text-gray-500 text-sm">
{t("costCurve")} {t("devSuffix")}
</span>
</div>
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 h-40 flex items-center justify-center">
<span className="text-gray-400 dark:text-gray-500 text-sm">
{t("nomadHeatmap")} {t("devSuffix")}
</span>
</div>
</div>
<div className="mt-6 rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4">
<h3 className="font-medium text-gray-900 dark:text-gray-100 mb-2">
{t("nearbyEvents")}
</h3>
<div className="flex gap-2 overflow-x-auto pb-2">
{[
{ city: "大理", date: "3月9日", emoji: "🏯" },
{ city: "成都", date: "3月12日", emoji: "🐼" },
{ city: "深圳", date: "3月14日", emoji: "🌃" },
].map((e) => (
<Link
key={e.city}
href="/meetups"
className="shrink-0 px-4 py-2 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700"
>
{e.emoji} {e.city} {e.date}
</Link>
))}
</div>
</div>
</main>
</div>
</div>
<CityModal
city={cityModal}
isOpen={!!cityModal}
onClose={() => setCityModal(null)}
/>
<Footer />
</div>
);
}

View File

@@ -1,8 +1,10 @@
"use client";
import { useState } from "react";
import { useTranslation } from "@/i18n/navigation";
import Footer from "@/app/components/Footer";
type TabType = "friends" | "dating";
type TabType = "friends" | "dating" | "partner" | "roommate" | "cofounder" | "explore";
interface Profile {
id: string;
@@ -44,7 +46,10 @@ const MOCK_MATCHES: MatchCard[] = [
{ id: "m5", name: "赵梦琪", location: "厦门", timeAgo: "1年前", initial: "赵", color: "bg-rose-400", photo: "https://i.pravatar.cc/150?img=41" },
];
const TAB_ORDER: TabType[] = ["friends", "dating", "partner", "roommate", "cofounder", "explore"];
export default function DatingPage() {
const { t } = useTranslation("match");
const [activeTab, setActiveTab] = useState<TabType>("friends");
const [currentIndex, setCurrentIndex] = useState(0);
@@ -59,36 +64,41 @@ export default function DatingPage() {
setCurrentIndex((i) => (hasMoreProfiles ? i + 1 : 0));
};
const badgeText = activeTab === "dating" ? "你们都在寻找 🌹 约会" : "你们都在寻找 🤝 交友";
const badgeMap: Record<TabType, string> = {
friends: t("badgeFriends"),
dating: t("badgeDating"),
partner: t("badgePartner"),
roommate: t("badgeRoommate"),
cofounder: t("badgeCofounder"),
explore: t("badgeExplore"),
};
const badgeText = badgeMap[activeTab];
return (
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
<div className="flex justify-center pt-6 pb-4 px-4">
<div className="inline-flex rounded-full bg-white dark:bg-gray-900 p-1 shadow-sm border border-gray-100 dark:border-gray-800">
<button
onClick={() => setActiveTab("friends")}
className={`px-6 py-2.5 rounded-full text-sm font-medium transition-all duration-200 ${
activeTab === "friends" ? "bg-[#ff4d4f] text-white shadow-sm" : "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100"
}`}
>
</button>
<button
onClick={() => setActiveTab("dating")}
className={`px-6 py-2.5 rounded-full text-sm font-medium transition-all duration-200 ${
activeTab === "dating" ? "bg-[#ff4d4f] text-white shadow-sm" : "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100"
}`}
>
</button>
<div className="flex flex-wrap justify-center gap-2 max-w-2xl">
{TAB_ORDER.map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-4 py-2 rounded-full text-xs sm:text-sm font-medium transition-all duration-200 ${
activeTab === tab
? "bg-[#ff4d4f] text-white shadow-sm"
: "bg-white dark:bg-gray-900 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 border border-gray-100 dark:border-gray-800"
}`}
>
{t(tab)}
</button>
))}
</div>
</div>
<div className="flex flex-col md:flex-row gap-6 max-w-7xl mx-auto px-4 pb-12">
<aside className="hidden md:block w-72 flex-shrink-0">
<div className="sticky top-24">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1"> 💕</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4"></p>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">{t("title")} 💕</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">{t("likedYou")}</p>
<div className="space-y-3">
{MOCK_MATCHES.map((match) => (
<div key={match.id} className="flex items-center gap-3 p-3 rounded-xl bg-white dark:bg-gray-900 border border-gray-100 dark:border-gray-800 shadow-sm hover:shadow-md transition-shadow cursor-pointer">
@@ -109,7 +119,7 @@ export default function DatingPage() {
<div className={`relative rounded-2xl overflow-hidden bg-gradient-to-br ${currentProfile.gradient} p-8 pb-6 shadow-xl border border-white/20`}>
<div className="absolute top-4 right-4 flex flex-col items-end gap-1">
<span className="px-3 py-1.5 rounded-full bg-white/90 text-gray-800 text-xs font-medium backdrop-blur-sm">{badgeText}</span>
<a href="#" className="text-[#ff4d4f] hover:text-red-600 text-sm font-medium"></a>
<a href="#" className="text-[#ff4d4f] hover:text-red-600 text-sm font-medium">{t("report")}</a>
</div>
<div className="flex justify-center mt-12 mb-6">
<img src={currentProfile.photo} alt={currentProfile.name} className="w-28 h-28 rounded-full shadow-lg border-4 border-white/50 object-cover" />
@@ -119,7 +129,7 @@ export default function DatingPage() {
<p className="text-white/90 text-sm mt-1">📍 {currentProfile.location}</p>
</div>
<div className="mb-4">
<p className="text-white/90 text-xs font-medium mb-2">:</p>
<p className="text-white/90 text-xs font-medium mb-2">{t("matchTags")}:</p>
<div className="flex flex-wrap gap-2 justify-center">
{currentProfile.tags.map((tag) => (
<span key={tag} className="px-3 py-1 rounded-full bg-white/80 text-gray-800 text-xs font-medium">{tag}</span>
@@ -129,16 +139,16 @@ export default function DatingPage() {
<p className="text-white/95 text-sm leading-relaxed text-center mb-8 px-2">{currentProfile.bio}</p>
<div className="flex items-center justify-center gap-8">
<div className="flex flex-col items-center gap-2">
<button onClick={handleDislike} className="w-16 h-16 rounded-full bg-white shadow-lg flex items-center justify-center text-[#ff4d4f] hover:bg-red-50 hover:scale-105 active:scale-95 transition-all" aria-label="不感兴趣">
<button onClick={handleDislike} className="w-16 h-16 rounded-full bg-white shadow-lg flex items-center justify-center text-[#ff4d4f] hover:bg-red-50 hover:scale-105 active:scale-95 transition-all" aria-label={t("dislike")}>
<span className="text-2xl"></span>
</button>
<span className="text-xs text-white/80 font-medium"></span>
<span className="text-xs text-white/80 font-medium">{t("dislike")}</span>
</div>
<div className="flex flex-col items-center gap-2">
<button onClick={handleLike} className="w-16 h-16 rounded-full bg-[#22c55e] shadow-lg flex items-center justify-center text-white hover:bg-[#16a34a] hover:scale-105 active:scale-95 transition-all" aria-label="喜欢">
<button onClick={handleLike} className="w-16 h-16 rounded-full bg-[#22c55e] shadow-lg flex items-center justify-center text-white hover:bg-[#16a34a] hover:scale-105 active:scale-95 transition-all" aria-label={t("like")}>
<span className="text-2xl">💚</span>
</button>
<span className="text-xs text-white/80 font-medium"></span>
<span className="text-xs text-white/80 font-medium">{t("like")}</span>
</div>
</div>
</div>
@@ -147,6 +157,7 @@ export default function DatingPage() {
</main>
<div className="hidden lg:block w-72 flex-shrink-0" />
</div>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,63 @@
"use client";
import { useTranslation } from "@/i18n/navigation";
import Footer from "@/app/components/Footer";
const MOCK_GIGS = [
{ id: "1", title: "帮我 P 图", city: "大理", reward: "¥50", author: "林**" },
{ id: "2", title: "找惠州 loft 民宿", city: "深圳", reward: "¥100", author: "陈**" },
{ id: "3", title: "代收快递", city: "成都", reward: "¥30", author: "王**" },
{ id: "4", title: "一起 citywalk 拍 neo2 航拍", city: "杭州", reward: "面议", author: "张**" },
{ id: "5", title: "翻译英文文档", city: "上海", reward: "¥200", author: "李**" },
];
export default function GigsPage() {
const { t } = useTranslation("gigs");
return (
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
<div className="max-w-4xl mx-auto px-4 sm:px-6 py-12">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100">
{t("title")}
</h1>
<p className="mt-1 text-gray-500 dark:text-gray-400">
{t("subtitle")} · {t("platformFee")}
</p>
</div>
<button className="shrink-0 px-6 py-3 rounded-xl bg-[#ff4d4f] text-white font-medium hover:bg-[#ff3333] transition-colors">
{t("publish")}
</button>
</div>
<div className="space-y-4">
{MOCK_GIGS.map((gig) => (
<div
key={gig.id}
className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 sm:p-5 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"
>
<div className="flex-1 min-w-0">
<h3 className="font-medium text-gray-900 dark:text-gray-100">
{gig.title}
</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
📍 {gig.city} · {gig.author}
</p>
</div>
<div className="flex items-center gap-3">
<span className="text-lg font-semibold text-[#ff4d4f]">
{gig.reward}
</span>
<button className="px-4 py-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700">
{t("browse")}
</button>
</div>
</div>
))}
</div>
</div>
<Footer />
</div>
);
}

View File

@@ -8,9 +8,9 @@ import Navbar from "../components/Navbar";
const LOCALES: Locale[] = ["zh", "en"];
export const metadata: Metadata = {
title: "游牧中国 - 数字游民的理想目的地",
title: "游牧中国 NomadCNA - 数字游民生活操作系统",
description:
"发现全球最适合远程工作和生活的城市,加入中国数字游民社区,探索远程办公新方式",
"数字游民的一站式生活操作系统:城市决策、税务签证、智能匹配、赏金任务。从 Free 到 Pro开启你的游牧人生",
};
export function generateStaticParams() {

View File

@@ -0,0 +1,109 @@
"use client";
import { useState } from "react";
import { useTranslation } from "@/i18n/navigation";
import { Link } from "@/i18n/navigation";
import Footer from "@/app/components/Footer";
const PLANS = [
{ id: "free", key: "free", price: 0, priceYear: 0, features: ["browse"], highlight: false },
{ id: "basic", key: "basic", price: 39, priceYear: 390, features: ["browse", "noAds", "advancedFilter", "unlimitedSwipe", "priorityEvent"], highlight: true },
{ id: "pro", key: "pro", price: 149, priceYear: 1490, features: ["browse", "noAds", "advancedFilter", "unlimitedSwipe", "priorityEvent", "crmRoster", "oneOnOne", "gigAccess", "dashboard"], highlight: false },
] as const;
export default function PricingPage() {
const { t } = useTranslation("pricing");
const [yearly, setYearly] = useState(false);
return (
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-12 sm:py-16">
<div className="text-center mb-12">
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100">
{t("title")}
</h1>
<p className="mt-2 text-gray-500 dark:text-gray-400">
{t("subtitle")}
</p>
<div className="mt-6 flex items-center justify-center gap-3">
<span className={`text-sm font-medium ${!yearly ? "text-gray-900 dark:text-gray-100" : "text-gray-500 dark:text-gray-400"}`}>
{t("monthly")}
</span>
<button
type="button"
onClick={() => setYearly(!yearly)}
className={`relative w-12 h-6 rounded-full transition-colors ${yearly ? "bg-[#ff4d4f]" : "bg-gray-200 dark:bg-gray-700"}`}
>
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${yearly ? "left-7" : "left-1"}`} />
</button>
<span className={`text-sm font-medium ${yearly ? "text-gray-900 dark:text-gray-100" : "text-gray-500 dark:text-gray-400"}`}>
{t("yearly")}
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8">
{PLANS.map((plan) => {
const displayPrice = yearly ? plan.priceYear : plan.price;
const priceKey = yearly ? "perYear" : "perMonth";
return (
<div
key={plan.id}
className={`rounded-2xl border p-6 sm:p-8 flex flex-col ${
plan.highlight
? "border-[#ff4d4f] bg-white dark:bg-gray-900 shadow-xl dark:shadow-none ring-2 ring-[#ff4d4f]/20"
: "border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900"
}`}
>
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">
{t(plan.key)}
</h2>
<div className="mt-4 flex items-baseline gap-1">
<span className="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-gray-100">
{displayPrice === 0 ? "¥0" : `¥${displayPrice.toLocaleString()}`}
</span>
{displayPrice > 0 && (
<span className="text-gray-500 dark:text-gray-400">
{t(priceKey)}
</span>
)}
</div>
{yearly && plan.priceYear > 0 && (
<p className="mt-1 text-xs text-green-600 dark:text-green-400">
¥{(plan.price * 12 - plan.priceYear).toLocaleString()}
</p>
)}
<ul className="mt-6 space-y-3 flex-1">
{plan.features.map((f) => (
<li
key={f}
className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300"
>
<span className="text-green-500"></span>
{t(`features.${f}`)}
</li>
))}
</ul>
<Link
href="/join"
className={`mt-6 w-full py-3 rounded-lg text-center font-medium transition-colors ${
plan.highlight
? "bg-[#ff4d4f] text-white hover:bg-[#ff3333]"
: "bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-700"
}`}
>
{plan.id === "free" ? t("getStarted") : t("upgrade")}
</Link>
</div>
);
})}
</div>
<p className="mt-8 text-center text-sm text-gray-500 dark:text-gray-400">
{t("features.experienceCamp")}
</p>
</div>
<Footer />
</div>
);
}

View File

@@ -0,0 +1,79 @@
"use client";
import { useState } from "react";
import { useTranslation } from "@/i18n/navigation";
import Footer from "@/app/components/Footer";
export default function ReportPage() {
const { t } = useTranslation("report");
const [generated, setGenerated] = useState(false);
return (
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
<div className="max-w-2xl mx-auto px-4 sm:px-6 py-12">
<div className="text-center mb-10">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100">
{t("title")}
</h1>
<p className="mt-2 text-gray-500 dark:text-gray-400">
{t("subtitle")}
</p>
</div>
{!generated ? (
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-8 text-center">
<div className="w-20 h-20 mx-auto rounded-full bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center text-3xl mb-6">
📊
</div>
<p className="text-gray-600 dark:text-gray-400 mb-6">
{t("desc")}
</p>
<button
onClick={() => setGenerated(true)}
className="px-6 py-3 rounded-xl bg-[#ff4d4f] text-white font-medium hover:bg-[#ff3333] transition-colors"
>
{t("generate")}
</button>
</div>
) : (
<div className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="h-2 bg-gradient-to-r from-amber-400 via-orange-500 to-red-500" />
<div className="p-6 sm:p-8">
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-6">
{t("yearTitle")}
</h2>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="p-4 rounded-xl bg-gray-50 dark:bg-gray-800">
<p className="text-2xl font-bold text-[#ff4d4f]">8</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{t("countries")}</p>
</div>
<div className="p-4 rounded-xl bg-gray-50 dark:bg-gray-800">
<p className="text-2xl font-bold text-[#ff4d4f]">¥5,200</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{t("avgCost")}</p>
</div>
<div className="p-4 rounded-xl bg-gray-50 dark:bg-gray-800">
<p className="text-2xl font-bold text-[#ff4d4f]">12</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{t("coworkingCheckins")}</p>
</div>
<div className="p-4 rounded-xl bg-gray-50 dark:bg-gray-800">
<p className="text-2xl font-bold text-[#ff4d4f]">3</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{t("meetups")}</p>
</div>
</div>
<p className="text-xs text-gray-400 dark:text-gray-500 text-center">
{t("watermark")}
</p>
<button
onClick={() => setGenerated(false)}
className="mt-4 w-full py-2 rounded-lg border border-gray-200 dark:border-gray-700 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800"
>
{t("regenerate")}
</button>
</div>
</div>
)}
</div>
<Footer />
</div>
);
}

131
app/[locale]/tools/page.tsx Normal file
View File

@@ -0,0 +1,131 @@
"use client";
import { useState } from "react";
import { useTranslation } from "@/i18n/navigation";
import Footer from "@/app/components/Footer";
const RATES = [
{ from: "CNY", to: "USD", rate: 0.14 },
{ from: "CNY", to: "EUR", rate: 0.13 },
{ from: "CNY", to: "THB", rate: 5.2 },
{ from: "USD", to: "CNY", rate: 7.2 },
];
export default function ToolsPage() {
const { t } = useTranslation("tools");
const [income, setIncome] = useState(100000);
const [cnyAmount, setCnyAmount] = useState(10000);
const [visaDate, setVisaDate] = useState("2025-06-15");
const taxEstimate = Math.round(income * 0.15);
const cnyToUsd = (v: number) => (v * 0.14).toFixed(2);
const daysLeft = Math.ceil(
(new Date(visaDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)
);
return (
<div className="min-h-screen bg-[#fafafa] dark:bg-gray-950">
<div className="max-w-4xl mx-auto px-4 sm:px-6 py-12">
<div className="mb-10">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100">
{t("title")}
</h1>
<p className="mt-1 text-gray-500 dark:text-gray-400">
{t("subtitle")}
</p>
</div>
<div className="space-y-6">
<section className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
💱 {t("exchange")}
</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{RATES.map((r) => (
<div
key={`${r.from}-${r.to}`}
className="p-4 rounded-xl bg-gray-50 dark:bg-gray-800"
>
<p className="text-xs text-gray-500 dark:text-gray-400">
{r.from} {r.to}
</p>
<p className="mt-1 text-lg font-bold text-gray-900 dark:text-gray-100">
{r.rate}
</p>
</div>
))}
</div>
<div className="mt-4 p-4 rounded-xl bg-gray-50 dark:bg-gray-800">
<label className="block text-xs text-gray-500 dark:text-gray-400 mb-2">CNY USD</label>
<div className="flex gap-2 items-center">
<input
type="number"
value={cnyAmount}
onChange={(e) => setCnyAmount(Number(e.target.value) || 0)}
className="flex-1 rounded-lg border border-gray-200 dark:border-gray-700 px-3 py-2 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
/>
<span className="text-gray-400"></span>
<span className="font-semibold text-gray-900 dark:text-gray-100">${cnyToUsd(cnyAmount)}</span>
</div>
</div>
<p className="mt-3 text-xs text-gray-400 dark:text-gray-500">
{t("disclaimer")}
</p>
</section>
<section className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
📊 {t("tax")}
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("incomeCountry")} ( ¥)
</label>
<input
type="number"
value={income}
onChange={(e) => setIncome(Number(e.target.value))}
className="w-full rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
/>
</div>
<div className="p-4 rounded-xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50">
<p className="text-sm text-amber-800 dark:text-amber-200">
{t("estimate")}: ¥{taxEstimate.toLocaleString()}/
</p>
<p className="mt-1 text-xs text-amber-600 dark:text-amber-300">
{t("taxDisclaimer")}
</p>
</div>
</div>
</section>
<section className="rounded-2xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
🛂 {t("visa")}
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("visaExpiry")}
</label>
<input
type="date"
value={visaDate}
onChange={(e) => setVisaDate(e.target.value)}
className="w-full rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
/>
</div>
<div className="p-4 rounded-xl bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800/50">
<p className="text-lg font-semibold text-emerald-800 dark:text-emerald-200">
{daysLeft > 0 ? `${daysLeft} ${t("daysLeft")}` : t("expired")}
</p>
</div>
</div>
</section>
</div>
</div>
<Footer />
</div>
);
}

View File

@@ -15,8 +15,9 @@ const footerSections = [
{
titleKey: "tools" as const,
links: [
{ labelKey: "climate" as const, href: "#" },
{ labelKey: "cost" as const, href: "#" },
{ labelKey: "dashboard" as const, href: "/dashboard" },
{ labelKey: "cost" as const, href: "/tools" },
{ labelKey: "ai" as const, href: "/ai" },
{ labelKey: "coworking" as const, href: "#" },
{ labelKey: "compare" as const, href: "#" },
],
@@ -27,6 +28,7 @@ const footerSections = [
{ labelKey: "guide" as const, href: "#" },
{ labelKey: "cityGuide" as const, href: "#" },
{ labelKey: "stories" as const, href: "#" },
{ labelKey: "report" as const, href: "/report" },
{ labelKey: "faq" as const, href: "#" },
],
},
@@ -34,6 +36,8 @@ const footerSections = [
titleKey: "about" as const,
links: [
{ labelKey: "aboutUs" as const, href: "/about" },
{ labelKey: "pricing" as const, href: "/pricing" },
{ labelKey: "gigs" as const, href: "/gigs" },
{ labelKey: "recruit" as const, href: "/jobs" },
{ labelKey: "terms" as const, href: "#" },
{ labelKey: "privacy" as const, href: "#" },

View File

@@ -2,6 +2,7 @@
import { useState } from "react";
import { Link } from "@/i18n/navigation";
import { useTranslation } from "@/i18n/navigation";
const avatarPhotos = [
"https://i.pravatar.cc/150?img=32",
@@ -18,14 +19,16 @@ const avatarPhotos = [
];
const features = [
{ icon: "🍹", text: "每年在 30+ 城市参加线下聚会" },
{ icon: "❤️", text: "结识新朋友,约会交友" },
{ icon: "🧪", text: "研究目的地,找到最适合居住和工作的地方" },
{ icon: "🌎", text: "追踪你的旅行,记录你去过的地方" },
{ icon: "💬", text: "加入社群聊天,在旅途中找到你的归属" },
{ icon: "📍", key: "feature1" as const },
{ icon: "📊", key: "feature2" as const },
{ icon: "👥", key: "feature3" as const },
{ icon: "💰", key: "feature4" as const },
{ icon: "📈", key: "feature5" as const },
];
export default function HeroSection() {
const { t } = useTranslation("common");
const { t: tHero } = useTranslation("hero");
const [email, setEmail] = useState("");
const handleJoin = (e: React.FormEvent) => {
@@ -57,29 +60,28 @@ export default function HeroSection() {
<div className="flex flex-col lg:flex-row lg:items-start lg:gap-10 xl:gap-16">
{/* Left Column */}
<div className="flex-1 min-w-0">
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-orange-700 bg-orange-100 px-3 py-1.5 rounded-full mb-5">
🏆 · 2024
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-orange-700 dark:text-orange-400 bg-orange-100 dark:bg-orange-900/30 px-3 py-1.5 rounded-full mb-5">
🏆 {tHero("badge")}
</span>
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight mb-5">
<span className="hero-gradient-text"></span>
<span className="hero-gradient-text">{tHero("title")}</span>
<br />
<span className="text-gray-900"></span>
<span className="text-gray-900 dark:text-gray-100">{tHero("titleSuffix")}</span>
</h1>
<p className="text-base sm:text-lg text-gray-500 max-w-2xl mb-7 leading-relaxed">
<p className="text-base sm:text-lg text-gray-500 dark:text-gray-400 max-w-2xl mb-7 leading-relaxed">
{tHero("subtitle")}
</p>
<div className="space-y-2.5 mb-8">
{features.map((f) => (
<div key={f.text} className="flex items-start gap-2.5">
<div key={f.key} className="flex items-start gap-2.5">
<span className="text-base sm:text-lg shrink-0 mt-0.5">
{f.icon}
</span>
<span className="text-sm sm:text-base text-gray-600 underline decoration-gray-300 underline-offset-2">
{f.text}
<span className="text-sm sm:text-base text-gray-600 dark:text-gray-400 underline decoration-gray-300 dark:decoration-gray-600 underline-offset-2">
{tHero(f.key)}
</span>
</div>
))}
@@ -92,26 +94,26 @@ export default function HeroSection() {
key={i}
src={src}
alt=""
className="w-8 h-8 rounded-full border-2 border-white shadow-sm object-cover"
className="w-8 h-8 rounded-full border-2 border-white dark:border-gray-800 shadow-sm object-cover"
/>
))}
</div>
<span className="text-sm text-gray-500">
<strong className="text-gray-900">5,000+</strong>
<span className="text-sm text-gray-500 dark:text-gray-400">
<strong className="text-gray-900 dark:text-gray-100">5,000+</strong>
</span>
</div>
<div className="pt-7 border-t border-gray-100">
<p className="text-xs text-gray-400 uppercase tracking-wider mb-3">
<div className="pt-7 border-t border-gray-100 dark:border-gray-800">
<p className="text-xs text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-3">
</p>
<div className="flex flex-wrap items-center gap-x-8 gap-y-2">
{["36氪", "少数派", "极客公园", "虎嗅", "创业邦", "澎湃新闻"].map(
(name) => (
<span
key={name}
className="text-sm font-semibold text-gray-300 hover:text-gray-400 transition-colors cursor-default"
>
<span
key={name}
className="text-sm font-semibold text-gray-300 dark:text-gray-600 hover:text-gray-400 dark:hover:text-gray-500 transition-colors cursor-default"
>
{name}
</span>
),
@@ -122,7 +124,7 @@ export default function HeroSection() {
{/* Right Column: Video + Email */}
<div className="lg:w-[360px] xl:w-[400px] shrink-0 mt-10 lg:mt-4">
<div className="bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-100">
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-xl overflow-hidden border border-gray-100 dark:border-gray-800">
<div className="relative h-48 sm:h-56 bg-gradient-to-br from-emerald-400 via-teal-500 to-cyan-600 flex items-center justify-center cursor-pointer group overflow-hidden">
<div className="absolute inset-0 opacity-20">
<div className="absolute top-4 left-4 w-20 h-16 rounded-lg bg-white/20" />
@@ -147,7 +149,7 @@ export default function HeroSection() {
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="输入你的邮箱..."
className="w-full border border-gray-200 rounded-lg px-4 py-3 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#ff4d4f]/20 focus:border-[#ff4d4f] transition-colors mb-3"
className="w-full border border-gray-200 dark:border-gray-700 rounded-lg px-4 py-3 text-sm placeholder-gray-400 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"
/>
<button
type="submit"
@@ -155,29 +157,35 @@ export default function HeroSection() {
>
</button>
<p className="text-xs text-gray-400 text-center mt-3">
<p className="text-xs text-gray-400 dark:text-gray-500 text-center mt-3">
</p>
</form>
</div>
<div className="mt-4 flex flex-wrap gap-2 justify-center">
<Link
href="/meetups"
className="text-xs text-gray-400 hover:text-gray-600 bg-white rounded-full px-3 py-1.5 border border-gray-100 transition-colors"
>
<Link href="/dashboard" className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 bg-white dark:bg-gray-800 rounded-full px-3 py-1.5 border border-gray-100 dark:border-gray-700 transition-colors">
📍
</Link>
<Link href="/meetups" className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 bg-white dark:bg-gray-800 rounded-full px-3 py-1.5 border border-gray-100 dark:border-gray-700 transition-colors">
🍹 线
</Link>
<Link
href="/dating"
className="text-xs text-gray-400 hover:text-gray-600 bg-white rounded-full px-3 py-1.5 border border-gray-100 transition-colors"
>
<Link href="/dating" className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 bg-white dark:bg-gray-800 rounded-full px-3 py-1.5 border border-gray-100 dark:border-gray-700 transition-colors">
</Link>
<Link
href="/join"
className="text-xs text-gray-400 hover:text-gray-600 bg-white rounded-full px-3 py-1.5 border border-gray-100 transition-colors"
>
<Link href="/tools" className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 bg-white dark:bg-gray-800 rounded-full px-3 py-1.5 border border-gray-100 dark:border-gray-700 transition-colors">
📊
</Link>
<Link href="/ai" className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 bg-white dark:bg-gray-800 rounded-full px-3 py-1.5 border border-gray-100 dark:border-gray-700 transition-colors">
🤖 AI
</Link>
<Link href="/gigs" className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 bg-white dark:bg-gray-800 rounded-full px-3 py-1.5 border border-gray-100 dark:border-gray-700 transition-colors">
💰
</Link>
<Link href="/report" className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 bg-white dark:bg-gray-800 rounded-full px-3 py-1.5 border border-gray-100 dark:border-gray-700 transition-colors">
📊
</Link>
<Link href="/join" className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 bg-white dark:bg-gray-800 rounded-full px-3 py-1.5 border border-gray-100 dark:border-gray-700 transition-colors">
💬
</Link>
</div>

View File

@@ -8,8 +8,13 @@ import ThemeToggle from "./ThemeToggle";
const navLinks = [
{ labelKey: "cities" as const, icon: "🏙️", href: "/" },
{ labelKey: "dashboard" as const, icon: "📍", href: "/dashboard" },
{ labelKey: "travel" as const, icon: "✈️", href: "/meetups" },
{ labelKey: "community" as const, icon: "💬", href: "/dating" },
{ labelKey: "tools" as const, icon: "📊", href: "/tools" },
{ labelKey: "ai" as const, icon: "🤖", href: "/ai" },
{ labelKey: "gigs" as const, icon: "💰", href: "/gigs" },
{ labelKey: "pricing" as const, icon: "👑", href: "/pricing" },
{ labelKey: "explore" as const, icon: "🎒", href: "/#community" },
];
@@ -48,7 +53,7 @@ export default function Navbar() {
</span>
</Link>
<div className="hidden md:flex items-center gap-1">
<div className="hidden md:flex items-center gap-1 overflow-x-auto max-w-[calc(100vw-280px)] scrollbar-hide">
{navLinks.map((link) => (
<Link
key={link.labelKey}

View File

@@ -1,13 +1,14 @@
/**
* 游牧中国站点配置
* 游牧中国站点配置 - Nomad OS
*/
export const SITE_CONFIG = {
meta: {
title: "游牧中国 - 数字游民的理想目的地",
title: "游牧中国 NomadCNA - 数字游民生活操作系统",
description:
"发现全球最适合远程工作和生活的城市,加入中国数字游民社区,探索远程办公新方式",
"数字游民的一站式生活操作系统:城市决策、税务签证、智能匹配、赏金任务。从 Free 到 Pro开启你的游牧人生",
},
tagline: "Nomad OS · 你的下一站,由数据决定",
};
export function getSiteConfig() {

View File

@@ -1,6 +1,7 @@
{
"common": {
"siteName": "Nomad China",
"tagline": "Nomad OS · Your next stop, data-driven",
"siteNameShort": "NomadCNA",
"logout": "Logout",
"loginRegister": "Login / Sign up",
@@ -9,15 +10,26 @@
},
"nav": {
"cities": "Cities",
"ai": "AI",
"travel": "Travel",
"community": "Community",
"explore": "Explore"
"explore": "Explore",
"dashboard": "Next Stop",
"tools": "Tools",
"pricing": "Pricing",
"gigs": "Gigs",
"report": "Report"
},
"hero": {
"badge": "China Digital Nomad Community · Since 2024",
"title": "Explore China",
"titleSuffix": "Remote Work, Free Life",
"subtitle": "Join the global Chinese digital nomad community, discover the best cities in China for remote work and living",
"badge": "Digital Nomad Life OS · Nomad OS",
"title": "Your Next Stop",
"titleSuffix": "Data-Driven",
"subtitle": "City decisions, tax & visa tools, smart matching, gig board. One-stop solution for nomad life",
"feature1": "Next stop: input budget, timezone, internet → get city rankings",
"feature2": "Tax & visa toolkit: exchange, tax estimate, visa countdown",
"feature3": "Smart match: partner, roommate, co-founder, explore buddy",
"feature4": "Gig board: post/claim tasks, platform fee 10-15%",
"feature5": "Nomad report: countries visited? Generate share card",
"ctaJoin": "Join Nomad China →",
"stats": {
"cities": "Cities",
@@ -32,9 +44,11 @@
"resources": "Resources",
"about": "About",
"links": {
"ai": "AI Assistant",
"join": "Join Community",
"meetups": "Offline Meetups",
"dating": "Dating",
"dating": "Smart Match",
"dashboard": "Next Stop",
"map": "Member Map",
"climate": "Climate Query",
"cost": "Cost Calculator",
@@ -43,8 +57,11 @@
"guide": "Remote Work Guide",
"cityGuide": "City Guide",
"stories": "Nomad Stories",
"report": "Nomad Report",
"faq": "FAQ",
"aboutUs": "About Us",
"pricing": "Pricing",
"gigs": "Gig Board",
"recruit": "Careers",
"terms": "Terms",
"privacy": "Privacy",
@@ -134,5 +151,112 @@
"goLogin": "Go to login",
"passwordMismatch": "Passwords do not match",
"passwordMinLength": "Password must be at least 8 characters"
},
"pricing": {
"title": "Membership",
"monthly": "Monthly",
"yearly": "Yearly",
"subtitle": "From Free to Pro, unlock more nomad power",
"free": "Free",
"basic": "Basic",
"pro": "Pro",
"perMonth": "/mo",
"perYear": "/yr",
"currentPlan": "Current plan",
"getStarted": "Get started",
"upgrade": "Upgrade",
"features": {
"browse": "Basic browse + public data",
"noAds": "Ad-free",
"advancedFilter": "Advanced filters",
"unlimitedSwipe": "Unlimited swipe",
"priorityEvent": "Priority event signup",
"crmRoster": "CRM roster visible",
"oneOnOne": "1v1 with paid guests",
"gigAccess": "Internal gig earnings",
"dashboard": "Data dashboard",
"experienceCamp": "Experience camp sold separately"
}
},
"dashboard": {
"title": "Next Stop Dashboard",
"noMatch": "No matching cities, try relaxing filters",
"devCostCurve": "Cost estimate curve",
"devHeatmap": "Nomad density heatmap",
"devSuffix": "(dev)",
"subtitle": "Input preferences, get city rankings",
"budget": "Monthly budget (¥)",
"timezone": "Timezone preference",
"internet": "Min internet (Mbps)",
"visa": "Visa friendliness",
"tags": "People to meet",
"search": "Recommend cities",
"costCurve": "Cost estimate curve",
"nomadHeatmap": "Nomad density heatmap",
"nearbyEvents": "Nearby events",
"aiTip": "AI recommendation"
},
"tools": {
"title": "Toolkit",
"disclaimer": "Data for reference only, actual rates may vary",
"taxDisclaimer": "Simplified estimate, not professional tax advice",
"subtitle": "Exchange · Tax · Visa",
"exchange": "Live exchange rates",
"tax": "Tax calculator",
"visa": "Visa countdown",
"incomeCountry": "Income country",
"estimate": "Estimate tax",
"visaExpiry": "Visa expiry",
"daysLeft": "days left",
"expired": "Expired"
},
"match": {
"title": "Smart Match",
"likedYou": "People who liked you",
"dislike": "Pass",
"like": "Like",
"report": "Report",
"matchTags": "Match tags",
"dating": "Dating",
"friends": "Friends",
"partner": "Partner",
"roommate": "Roommate",
"cofounder": "Co-founder",
"explore": "Explore buddy",
"badgeDating": "Both looking for 🌹 Dating",
"badgeFriends": "Both looking for 🤝 Friends",
"badgePartner": "Both looking for 👥 Partner",
"badgeRoommate": "Both looking for 🏠 Roommate",
"badgeCofounder": "Both looking for 🚀 Co-founder",
"badgeExplore": "Both looking for 🍜 Explore"
},
"gigs": {
"title": "Gig Board",
"subtitle": "Post tasks · Earn money",
"publish": "Post task",
"browse": "Browse tasks",
"platformFee": "Platform fee 10-15%"
},
"ai": {
"title": "AI Assistant",
"subtitle": "Ask for city recommendations, travel plans, intro copy",
"placeholder": "Enter your question, e.g. Budget $800/mo, Asia tropical, stable internet...",
"ask": "Ask",
"thinking": "Thinking…",
"examples": "Example questions",
"comingSoon": "AI feature coming soon"
},
"report": {
"title": "Nomad Year Report",
"desc": "Auto stats: countries visited, avg monthly cost, coworking check-ins",
"yearTitle": "2025 Nomad Report",
"countries": "Countries",
"avgCost": "Avg monthly cost",
"coworkingCheckins": "Coworking check-ins",
"meetups": "Meetups",
"watermark": "Nomad China · Share card with watermark",
"regenerate": "Regenerate",
"subtitle": "Countries visited? Avg monthly cost?",
"generate": "Generate report"
}
}

View File

@@ -1,6 +1,7 @@
{
"common": {
"siteName": "游牧中国",
"tagline": "Nomad OS · 你的下一站,由数据决定",
"siteNameShort": "游牧中国",
"logout": "退出",
"loginRegister": "登录 / 注册",
@@ -9,15 +10,26 @@
},
"nav": {
"cities": "城市",
"ai": "AI",
"travel": "旅行",
"community": "社区",
"explore": "探索"
"explore": "探索",
"dashboard": "下一站",
"tools": "工具箱",
"pricing": "会员",
"gigs": "赏金",
"report": "年报"
},
"hero": {
"badge": "中国数字游民社区 · 始于 2024",
"title": "探索中国",
"titleSuffix": "远程工作,自由生活",
"subtitle": "加入全球华人数字游民社区,发现中国最适合远程工作和生活的城市",
"badge": "数字游民生活操作系统 · Nomad OS",
"title": "你的下一站",
"titleSuffix": "由数据决定",
"subtitle": "城市决策、税务签证、智能匹配、赏金任务。一站式解决游牧生活的核心痛点",
"feature1": "下一站决策:输入预算、时区、网速,输出城市排名",
"feature2": "税务签证工具箱:汇率、税负估算、签证倒计时",
"feature3": "智能匹配:搭档、室友、合伙人、探店 buddy",
"feature4": "赏金墙:发布/接单小任务,平台抽成 10-15%",
"feature5": "Nomad 年报:今年飞了多少国家?生成分享卡片",
"ctaJoin": "加入游牧中国 →",
"stats": {
"cities": "个城市",
@@ -32,9 +44,11 @@
"resources": "资源",
"about": "关于",
"links": {
"ai": "AI 助理",
"join": "加入社群",
"meetups": "线下聚会",
"dating": "交友约会",
"dating": "智能匹配",
"dashboard": "下一站",
"map": "成员地图",
"climate": "气候查询",
"cost": "生活成本计算",
@@ -43,8 +57,11 @@
"guide": "远程工作指南",
"cityGuide": "城市落地攻略",
"stories": "游民故事",
"report": "Nomad 年报",
"faq": "常见问题",
"aboutUs": "关于我们",
"pricing": "会员定价",
"gigs": "赏金墙",
"recruit": "招聘",
"terms": "使用条款",
"privacy": "隐私政策",
@@ -134,5 +151,112 @@
"goLogin": "去登录",
"passwordMismatch": "两次密码不一致",
"passwordMinLength": "密码至少 8 位"
},
"pricing": {
"title": "会员体系",
"monthly": "月付",
"yearly": "年付",
"subtitle": "从 Free 到 Pro解锁更多游牧能力",
"free": "Free",
"basic": "Basic",
"pro": "Pro",
"perMonth": "/月",
"perYear": "/年",
"currentPlan": "当前方案",
"getStarted": "开始使用",
"upgrade": "升级",
"features": {
"browse": "基础浏览 + 公开资料",
"noAds": "去除广告",
"advancedFilter": "高级筛选",
"unlimitedSwipe": "无限滑动匹配",
"priorityEvent": "优先活动报名",
"crmRoster": "专属花名册可见",
"oneOnOne": "付费嘉宾 1v1",
"gigAccess": "内部小任务赚钱",
"dashboard": "数据仪表盘",
"experienceCamp": "体验营/共居单独售卖"
}
},
"dashboard": {
"title": "下一站决策仪表盘",
"noMatch": "暂无匹配城市,放宽筛选条件试试",
"devCostCurve": "成本预估曲线",
"devHeatmap": "游民密度热力图",
"devSuffix": "(开发中)",
"subtitle": "输入偏好,输出最适合你的城市排名",
"budget": "月预算 (¥)",
"timezone": "时区偏好",
"internet": "最低网速 (Mbps)",
"visa": "签证友好度",
"tags": "想认识的人群",
"search": "推荐城市",
"costCurve": "成本预估曲线",
"nomadHeatmap": "游民密度热力图",
"nearbyEvents": "最近同城活动",
"aiTip": "AI 推荐"
},
"tools": {
"title": "工具箱",
"disclaimer": "数据仅供参考,以实际汇率为准",
"taxDisclaimer": "简化估算,非专业税务建议",
"subtitle": "汇率 · 税务 · 签证",
"exchange": "实时汇率",
"tax": "税务计算器",
"visa": "签证倒计时",
"incomeCountry": "收入国家",
"estimate": "估算税负",
"visaExpiry": "签证到期",
"daysLeft": "天剩余",
"expired": "已过期"
},
"match": {
"title": "智能匹配",
"likedYou": "这些人喜欢了你",
"dislike": "不感兴趣",
"like": "喜欢",
"report": "举报资料",
"matchTags": "匹配标签",
"dating": "约会",
"friends": "交友",
"partner": "搭档",
"roommate": "室友",
"cofounder": "项目合伙人",
"explore": "探店 buddy",
"badgeDating": "你们都在寻找 🌹 约会",
"badgeFriends": "你们都在寻找 🤝 交友",
"badgePartner": "你们都在寻找 👥 搭档",
"badgeRoommate": "你们都在寻找 🏠 室友",
"badgeCofounder": "你们都在寻找 🚀 合伙人",
"badgeExplore": "你们都在寻找 🍜 探店"
},
"gigs": {
"title": "赏金墙",
"subtitle": "发布需求 · 接单赚钱",
"publish": "发布任务",
"browse": "浏览任务",
"platformFee": "平台抽成 10-15%"
},
"ai": {
"title": "AI 助理",
"subtitle": "问城市推荐、写行程计划、生成介绍文案",
"placeholder": "输入你的问题例如预算每月800刀想亚洲热带有稳定网...",
"ask": "提问",
"thinking": "思考中…",
"examples": "示例问题",
"comingSoon": "AI 功能即将上线,敬请期待"
},
"report": {
"title": "Nomad 年报",
"desc": "自动统计:今年飞了多少国家、平均月生活成本、在多少个 co-working 打卡",
"yearTitle": "2025 Nomad 年报",
"countries": "国家/地区",
"avgCost": "月均生活成本",
"coworkingCheckins": "Co-working 打卡",
"meetups": "线下聚会",
"watermark": "游牧中国 NomadCNA · 生成分享卡片带水印引流",
"regenerate": "重新生成",
"subtitle": "今年飞了多少国家?平均月生活成本?",
"generate": "生成年报"
}
}