's'
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { Link } from "@/i18n/navigation";
|
import { Link, useTranslation } from "@/i18n/navigation";
|
||||||
import { VideoPlayer } from "@/app/components/course/VideoPlayer";
|
import { VideoPlayer } from "@/app/components/course/VideoPlayer";
|
||||||
import { LessonMarkdown } from "./LessonMarkdown";
|
import { LessonMarkdown } from "./LessonMarkdown";
|
||||||
import { getLesson } from "@/config/lessons";
|
import { getLesson } from "@/config/lessons";
|
||||||
@@ -14,6 +14,8 @@ import Header from "@/app/components/Header";
|
|||||||
export default function LessonPage() {
|
export default function LessonPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation("community");
|
||||||
|
const [toast, setToast] = useState(false);
|
||||||
const moduleIndex = Number(params.moduleIndex);
|
const moduleIndex = Number(params.moduleIndex);
|
||||||
const lessonIndex = Number(params.lessonIndex);
|
const lessonIndex = Number(params.lessonIndex);
|
||||||
|
|
||||||
@@ -149,6 +151,14 @@ export default function LessonPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
{toast && (
|
||||||
|
<div
|
||||||
|
className="fixed bottom-8 left-1/2 z-50 -translate-x-1/2 rounded-lg bg-slate-800 px-6 py-3 text-sm font-medium text-white shadow-lg dark:bg-slate-700"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{t("updating")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export default async function DayPage({ params }: PageProps) {
|
|||||||
🌍 数字游民指南
|
🌍 数字游民指南
|
||||||
</Link>
|
</Link>
|
||||||
<span className="mx-2">·</span>
|
<span className="mx-2">·</span>
|
||||||
开源免费 · 社区驱动
|
精选内容 · 持续更新
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import Roadmap from "../components/Roadmap";
|
|||||||
import Tools from "../components/Tools";
|
import Tools from "../components/Tools";
|
||||||
import Community from "../components/Community";
|
import Community from "../components/Community";
|
||||||
import Footer from "../components/Footer";
|
import Footer from "../components/Footer";
|
||||||
import { getThemeToolsData } from "../lib/theme-data";
|
import { getThemeToolsData, getResourceData } from "../lib/theme-data";
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const toolsData = await getThemeToolsData();
|
const [toolsData, resourceData] = await Promise.all([
|
||||||
|
getThemeToolsData(),
|
||||||
|
getResourceData(),
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -18,7 +21,7 @@ export default async function HomePage() {
|
|||||||
<Features />
|
<Features />
|
||||||
<Roadmap />
|
<Roadmap />
|
||||||
<Tools data={toolsData} />
|
<Tools data={toolsData} />
|
||||||
<Community />
|
<Community resourceData={resourceData} />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,84 +1,58 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { Link, useTranslation } from "@/i18n/navigation";
|
import { Link, useTranslation } from "@/i18n/navigation";
|
||||||
import ShopCard from "./ShopCard";
|
|
||||||
import { TOPIC_IDS } from "@/config";
|
import { TOPIC_IDS } from "@/config";
|
||||||
|
import type { ResourceData } from "@/app/lib/theme-data";
|
||||||
|
import ResourceNavigation from "./ResourceNavigation";
|
||||||
|
|
||||||
const linkKeys = ["github", "destinations", "indieDev", "aiAgent", "cloudPhone", "unmannedLive", "appDownload"] as const;
|
const mainModuleKeys = ["destinations", "vipEntry", "appDownload"] as const;
|
||||||
|
const topicKeys = ["indieDev", "aiAgent", "cloudPhone", "unmannedLive"] as const;
|
||||||
|
|
||||||
const linkIcons: Record<(typeof linkKeys)[number], string> = {
|
const linkIcons: Record<string, string> = {
|
||||||
github: "📦",
|
|
||||||
destinations: "🗺️",
|
destinations: "🗺️",
|
||||||
|
vipEntry: "👑",
|
||||||
|
appDownload: "📱",
|
||||||
indieDev: "🛠️",
|
indieDev: "🛠️",
|
||||||
aiAgent: "🤖",
|
aiAgent: "🤖",
|
||||||
cloudPhone: "☁️",
|
cloudPhone: "☁️",
|
||||||
unmannedLive: "📺",
|
unmannedLive: "📺",
|
||||||
appDownload: "📱",
|
|
||||||
};
|
};
|
||||||
const linkHrefs: Record<(typeof linkKeys)[number], { href: string; newTab?: boolean }> = {
|
const linkHrefs: Record<string, { href: string; newTab?: boolean }> = {
|
||||||
github: { href: "https://github.com" },
|
|
||||||
destinations: { href: "https://meetup.hackrobot.cn/", newTab: true },
|
destinations: { href: "https://meetup.hackrobot.cn/", newTab: true },
|
||||||
indieDev: { href: "/ebook" },
|
vipEntry: { href: "https://vip.hackrobot.cn/", newTab: true },
|
||||||
aiAgent: { href: "/ebook" },
|
|
||||||
cloudPhone: { href: "https://android.hackrobot.cn", newTab: true },
|
|
||||||
unmannedLive: { href: "/ebook" },
|
|
||||||
appDownload: { href: "/app" },
|
appDownload: { href: "/app" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Community() {
|
type CommunityProps = {
|
||||||
|
resourceData?: ResourceData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Community({ resourceData }: CommunityProps) {
|
||||||
const { t } = useTranslation("community");
|
const { t } = useTranslation("community");
|
||||||
|
const [toast, setToast] = useState(false);
|
||||||
|
|
||||||
|
const showUpdating = () => {
|
||||||
|
setToast(true);
|
||||||
|
setTimeout(() => setToast(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCardContent = (key: string) => (
|
||||||
|
<>
|
||||||
|
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-slate-50 text-xl dark:bg-slate-800">
|
||||||
|
{linkIcons[key]}
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h4 className="font-bold text-slate-900 dark:text-slate-100">{t(`links.${key}.title`)}</h4>
|
||||||
|
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">{t(`links.${key}.desc`)}</p>
|
||||||
|
<span className="mt-2 inline-block text-sm font-medium text-sky-600">{t(`links.${key}.label`)}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="community" className="py-20 sm:py-28 dark:bg-slate-950">
|
<section id="community" className="py-20 sm:py-28 dark:bg-slate-950">
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
{/* 电商独立站卡片 */}
|
|
||||||
<div id="shop">
|
|
||||||
<ShopCard />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 数字游民社区 - 优化布局 */}
|
|
||||||
<div className="mb-20">
|
|
||||||
<Link
|
|
||||||
href="/join"
|
|
||||||
className="group block overflow-hidden rounded-2xl border border-sky-200/80 bg-gradient-to-br from-sky-50 via-white to-cyan-50/60 p-8 shadow-sm transition-all duration-300 hover:border-sky-300 hover:shadow-xl hover:shadow-sky-100/50 dark:border-sky-800/80 dark:from-sky-900/30 dark:via-slate-900 dark:to-cyan-900/20 dark:hover:border-sky-700 dark:hover:shadow-sky-900/30 sm:p-10"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="mb-3 inline-flex items-center gap-2 rounded-full bg-sky-100 px-4 py-1.5 text-sm font-semibold text-sky-700 dark:bg-sky-900/50 dark:text-sky-300">
|
|
||||||
<span className="text-lg">👥</span>
|
|
||||||
{t("nomadCommunity")}
|
|
||||||
</div>
|
|
||||||
<h3 className="mb-3 text-2xl font-bold tracking-tight text-slate-900 dark:text-slate-100 sm:text-3xl">
|
|
||||||
{t("joinTitle")}
|
|
||||||
</h3>
|
|
||||||
<p className="mb-4 max-w-xl text-base leading-relaxed text-slate-600 dark:text-slate-400">
|
|
||||||
{t("joinDesc")}
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
<span className="rounded-lg bg-white/90 px-3 py-1.5 text-sm font-medium text-slate-600 shadow-sm dark:bg-slate-800/90 dark:text-slate-300">
|
|
||||||
{t("applyForm")}
|
|
||||||
</span>
|
|
||||||
<span className="rounded-lg bg-white/90 px-3 py-1.5 text-sm font-medium text-slate-600 shadow-sm dark:bg-slate-800/90 dark:text-slate-300">
|
|
||||||
{t("approveJoin")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex shrink-0 items-center gap-4 rounded-2xl bg-white/90 p-6 shadow-md backdrop-blur-sm transition-transform dark:bg-slate-800/90 group-hover:scale-[1.02] sm:p-8">
|
|
||||||
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-400 to-cyan-500 text-3xl shadow-lg shadow-sky-200/50">
|
|
||||||
📝
|
|
||||||
</div>
|
|
||||||
<div className="text-left">
|
|
||||||
<div className="font-bold text-slate-800 dark:text-slate-200">
|
|
||||||
{t("applyNow")}
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
|
||||||
{t("applyHint")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="resources" className="text-center">
|
<div id="resources" className="text-center">
|
||||||
<h2 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-slate-100 sm:text-4xl">
|
<h2 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-slate-100 sm:text-4xl">
|
||||||
<span className="gradient-text">{t("resourcesTitle")}</span>
|
<span className="gradient-text">{t("resourcesTitle")}</span>
|
||||||
@@ -90,44 +64,64 @@ export default function Community() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
{/* 图书 / 商业数码 / 网盘 */}
|
||||||
{linkKeys.map((key) => {
|
{resourceData && <ResourceNavigation data={resourceData} />}
|
||||||
|
|
||||||
|
{/* 目的地数据库 + 私域VIP入口 + App 下载 - 主模块 */}
|
||||||
|
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{mainModuleKeys.map((key) => {
|
||||||
const { href, newTab } = linkHrefs[key];
|
const { href, newTab } = linkHrefs[key];
|
||||||
const isTopic = (TOPIC_IDS as readonly string[]).includes(key);
|
|
||||||
const content = (
|
|
||||||
<>
|
|
||||||
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-slate-50 text-xl dark:bg-slate-800">
|
|
||||||
{linkIcons[key]}
|
|
||||||
</span>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<h4 className="font-bold text-slate-900 dark:text-slate-100">{t(`links.${key}.title`)}</h4>
|
|
||||||
{isTopic && (
|
|
||||||
<span className="rounded-md bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/50 dark:text-amber-400">
|
|
||||||
{t("topicTag")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">{t(`links.${key}.desc`)}</p>
|
|
||||||
<span className="mt-2 inline-block text-sm font-medium text-sky-600">
|
|
||||||
{t(`links.${key}.label`)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={key}
|
key={key}
|
||||||
href={href}
|
href={href}
|
||||||
target={newTab ? "_blank" : undefined}
|
target={newTab ? "_blank" : undefined}
|
||||||
rel={newTab ? "noopener noreferrer" : undefined}
|
rel={newTab ? "noopener noreferrer" : undefined}
|
||||||
className="card-hover flex items-start gap-4 rounded-xl border border-slate-100 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
className="card-hover flex items-start gap-5 rounded-2xl border-2 border-slate-200 bg-white p-6 shadow-md transition-all hover:border-sky-300 hover:shadow-lg dark:border-slate-700 dark:bg-slate-900 dark:hover:border-sky-600"
|
||||||
>
|
>
|
||||||
{content}
|
{renderCardContent(key)}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 4 个专题 - 小卡片,暂不可点击 */}
|
||||||
|
<div className="mt-8">
|
||||||
|
<h3 className="mb-4 text-center text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
|
||||||
|
{t("topicTag")}
|
||||||
|
</h3>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{topicKeys.map((key) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
type="button"
|
||||||
|
onClick={showUpdating}
|
||||||
|
className="flex cursor-not-allowed items-start gap-4 rounded-xl border border-slate-200 bg-slate-50/80 p-4 opacity-90 transition-opacity hover:opacity-100 dark:border-slate-700 dark:bg-slate-800/60"
|
||||||
|
>
|
||||||
|
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-slate-200 text-lg dark:bg-slate-700">
|
||||||
|
{linkIcons[key]}
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0 flex-1 text-left">
|
||||||
|
<h4 className="font-bold text-slate-700 dark:text-slate-300">{t(`links.${key}.title`)}</h4>
|
||||||
|
<p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">{t(`links.${key}.desc`)}</p>
|
||||||
|
<span className="mt-1.5 inline-block text-xs font-medium text-amber-600 dark:text-amber-400">
|
||||||
|
{t("updating")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 更新中 Toast */}
|
||||||
|
{toast && (
|
||||||
|
<div
|
||||||
|
className="fixed bottom-8 left-1/2 z-50 -translate-x-1/2 rounded-lg bg-slate-800 px-6 py-3 text-sm font-medium text-white shadow-lg dark:bg-slate-700"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{t("updating")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,13 +40,6 @@ export default function Features() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 text-center">
|
|
||||||
<div className="inline-flex items-center gap-2 rounded-full border border-amber-200 bg-amber-50 px-5 py-2 text-sm font-medium text-amber-700 dark:border-amber-800 dark:bg-amber-900/50 dark:text-amber-300">
|
|
||||||
<span>🔥</span>
|
|
||||||
{t("stat")} <span className="font-bold">{t("statCount")}</span> {t("statSuffix")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ const footerSections = [
|
|||||||
titleKey: "community" as const,
|
titleKey: "community" as const,
|
||||||
links: [
|
links: [
|
||||||
{ labelKey: "discord" as const, href: "#" },
|
{ labelKey: "discord" as const, href: "#" },
|
||||||
{ labelKey: "wechat" as const, href: "#community" },
|
{ labelKey: "wechat" as const, href: "https://vip.hackrobot.cn/", openInNewTab: true },
|
||||||
{ labelKey: "jike" as const, href: "#" },
|
{ labelKey: "jike" as const, href: "#" },
|
||||||
{ labelKey: "github" as const, href: "#" },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import ThemeToggle from "./ThemeToggle";
|
|||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ labelKey: "roadmap" as const, href: "#roadmap" },
|
{ labelKey: "roadmap" as const, href: "#roadmap" },
|
||||||
{ labelKey: "tools" as const, href: "#tools" },
|
{ labelKey: "tools" as const, href: "#tools" },
|
||||||
{ labelKey: "community" as const, href: "#community" },
|
{ labelKey: "community" as const, href: "https://vip.hackrobot.cn/", external: true },
|
||||||
{ labelKey: "shop" as const, href: "#shop" },
|
|
||||||
{ labelKey: "resources" as const, href: "#resources" },
|
{ labelKey: "resources" as const, href: "#resources" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -54,6 +53,8 @@ export default function Header() {
|
|||||||
<a
|
<a
|
||||||
key={link.href}
|
key={link.href}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
|
target={"external" in link && link.external ? "_blank" : undefined}
|
||||||
|
rel={"external" in link && link.external ? "noopener noreferrer" : undefined}
|
||||||
className="rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
|
className="rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
|
||||||
>
|
>
|
||||||
{tNav(link.labelKey)}
|
{tNav(link.labelKey)}
|
||||||
@@ -123,6 +124,8 @@ export default function Header() {
|
|||||||
<a
|
<a
|
||||||
key={link.href}
|
key={link.href}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
|
target={"external" in link && link.external ? "_blank" : undefined}
|
||||||
|
rel={"external" in link && link.external ? "noopener noreferrer" : undefined}
|
||||||
onClick={() => setMobileOpen(false)}
|
onClick={() => setMobileOpen(false)}
|
||||||
className="block rounded-lg px-3 py-2.5 text-base font-medium text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
|
className="block rounded-lg px-3 py-2.5 text-base font-medium text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ import { useTranslation } from "@/i18n/navigation";
|
|||||||
|
|
||||||
export default function Hero() {
|
export default function Hero() {
|
||||||
const { t } = useTranslation("hero");
|
const { t } = useTranslation("hero");
|
||||||
const stats = [
|
|
||||||
{ value: "200+", labelKey: "resources" as const },
|
|
||||||
{ value: "7 天", labelKey: "path" as const },
|
|
||||||
{ value: "50+", labelKey: "tools" as const },
|
|
||||||
{ value: "30+", labelKey: "destinations" as const },
|
|
||||||
{ value: "100%", labelKey: "free" as const },
|
|
||||||
];
|
|
||||||
return (
|
return (
|
||||||
<section className="relative overflow-hidden pt-24 pb-16 sm:pt-32 sm:pb-20">
|
<section className="relative overflow-hidden pt-24 pb-16 sm:pt-32 sm:pb-20">
|
||||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-sky-50/80 via-white to-white dark:from-slate-900 dark:via-slate-900 dark:to-slate-950" />
|
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-sky-50/80 via-white to-white dark:from-slate-900 dark:via-slate-900 dark:to-slate-950" />
|
||||||
@@ -53,43 +46,6 @@ export default function Hero() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats marquee */}
|
|
||||||
<div className="marquee-container mt-16 overflow-hidden">
|
|
||||||
<div className="flex animate-scroll w-max gap-6">
|
|
||||||
{[...stats, ...stats].map((s, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="flex shrink-0 items-center gap-3 rounded-2xl border border-slate-100 bg-white/80 px-6 py-4 shadow-sm backdrop-blur-sm dark:border-slate-700 dark:bg-slate-800/80"
|
|
||||||
>
|
|
||||||
<span className="text-2xl font-bold text-sky-600 sm:text-3xl">
|
|
||||||
{s.value}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium text-slate-500 dark:text-slate-400">
|
|
||||||
{t(`stats.${s.labelKey}`)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="marquee-container mt-4 overflow-hidden">
|
|
||||||
<div className="flex animate-scroll-reverse w-max gap-6">
|
|
||||||
{[...stats, ...stats].map((s, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="flex shrink-0 items-center gap-3 rounded-2xl border border-slate-100 bg-white/80 px-6 py-4 shadow-sm backdrop-blur-sm dark:border-slate-700 dark:bg-slate-800/80"
|
|
||||||
>
|
|
||||||
<span className="text-2xl font-bold text-amber-500 sm:text-3xl">
|
|
||||||
{s.value}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium text-slate-500 dark:text-slate-400">
|
|
||||||
{t(`stats.${s.labelKey}`)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
275
app/components/ResourceNavigation.tsx
Normal file
275
app/components/ResourceNavigation.tsx
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useMemo, useEffect } from "react";
|
||||||
|
import { useTranslation } from "@/i18n/navigation";
|
||||||
|
import type { ResourceData } from "@/app/lib/theme-data";
|
||||||
|
|
||||||
|
const PER_PAGE_MOBILE = 4;
|
||||||
|
const PER_PAGE_PC = 8;
|
||||||
|
const BREAKPOINT = 768;
|
||||||
|
|
||||||
|
function useIsPC() {
|
||||||
|
const [isPC, setIsPC] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const mq = window.matchMedia(`(min-width: ${BREAKPOINT}px)`);
|
||||||
|
setIsPC(mq.matches);
|
||||||
|
const fn = () => setIsPC(mq.matches);
|
||||||
|
mq.addEventListener("change", fn);
|
||||||
|
return () => mq.removeEventListener("change", fn);
|
||||||
|
}, []);
|
||||||
|
return isPC;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 每页条数:H5=4,PC=8;总数不足则全显 */
|
||||||
|
function getPerPage(total: number, isPC: boolean): number {
|
||||||
|
const cap = isPC ? PER_PAGE_PC : PER_PAGE_MOBILE;
|
||||||
|
return total <= cap ? total : cap;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceNavigationProps = {
|
||||||
|
data: ResourceData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ResourceNavigation({ data }: ResourceNavigationProps) {
|
||||||
|
const { t } = useTranslation("community");
|
||||||
|
const isPC = useIsPC();
|
||||||
|
const firstCategoryId = data.pan.categories[0]?.id ?? "";
|
||||||
|
const [activePanCategory, setActivePanCategory] = useState(firstCategoryId);
|
||||||
|
const [booksPage, setBooksPage] = useState(0);
|
||||||
|
const [productsPage, setProductsPage] = useState(0);
|
||||||
|
const [toast, setToast] = useState(false);
|
||||||
|
|
||||||
|
const showUpdating = () => {
|
||||||
|
setToast(true);
|
||||||
|
setTimeout(() => setToast(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeCategory = data.pan.categories.find((c) => c.id === activePanCategory);
|
||||||
|
|
||||||
|
const booksPerPage = getPerPage(data.books.length, isPC);
|
||||||
|
const productsPerPage = getPerPage(data.products.length, isPC);
|
||||||
|
const booksTotalPages = Math.ceil(data.books.length / booksPerPage) || 1;
|
||||||
|
const productsTotalPages = Math.ceil(data.products.length / productsPerPage) || 1;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBooksPage((p) => Math.max(0, Math.min(p, booksTotalPages - 1)));
|
||||||
|
}, [booksTotalPages]);
|
||||||
|
useEffect(() => {
|
||||||
|
setProductsPage((p) => Math.max(0, Math.min(p, productsTotalPages - 1)));
|
||||||
|
}, [productsTotalPages]);
|
||||||
|
|
||||||
|
const paginatedBooks = useMemo(() => {
|
||||||
|
const start = booksPage * booksPerPage;
|
||||||
|
return data.books.slice(start, start + booksPerPage);
|
||||||
|
}, [data.books, booksPage, booksPerPage]);
|
||||||
|
|
||||||
|
const paginatedProducts = useMemo(() => {
|
||||||
|
const start = productsPage * productsPerPage;
|
||||||
|
return data.products.slice(start, start + productsPerPage);
|
||||||
|
}, [data.products, productsPage, productsPerPage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-12 space-y-16">
|
||||||
|
{/* 图书 - 封面+书名,数量少则大封面,多则分页 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-4 flex items-center gap-2 text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||||
|
<span>📚</span>
|
||||||
|
{t("resourceSections.books")}
|
||||||
|
{data.books.length > 0 && (
|
||||||
|
<span className="text-sm font-normal text-slate-500 dark:text-slate-400">
|
||||||
|
({t("resourceSections.totalBooks").replace("{count}", String(data.books.length))})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-2 gap-3 gap-y-4 sm:gap-4 md:grid-cols-8"
|
||||||
|
>
|
||||||
|
{paginatedBooks.map((book, i) => (
|
||||||
|
<button
|
||||||
|
key={`book-${booksPage}-${i}`}
|
||||||
|
type="button"
|
||||||
|
onClick={showUpdating}
|
||||||
|
className="group flex cursor-not-allowed flex-col text-left"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="overflow-hidden rounded-lg border border-slate-200 bg-slate-100 shadow-sm transition-all group-hover:shadow-md dark:border-slate-700 dark:bg-slate-800"
|
||||||
|
style={{ aspectRatio: "2/3" }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={book.cover}
|
||||||
|
alt={book.title}
|
||||||
|
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 line-clamp-2 text-center text-xs font-medium text-slate-700 dark:text-slate-300 sm:text-sm">
|
||||||
|
{book.title}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{booksTotalPages > 1 && (
|
||||||
|
<div className="mt-6 flex items-center justify-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setBooksPage((p) => Math.max(0, p - 1))}
|
||||||
|
disabled={booksPage === 0}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
|
||||||
|
aria-label="上一页"
|
||||||
|
>
|
||||||
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span className="px-4 text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
{booksPage + 1} / {booksTotalPages}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setBooksPage((p) => Math.min(booksTotalPages - 1, p + 1))}
|
||||||
|
disabled={booksPage === booksTotalPages - 1}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
|
||||||
|
aria-label="下一页"
|
||||||
|
>
|
||||||
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 商业数码 - 商品图+跳转电商 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-4 flex items-center gap-2 text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||||
|
<span>🛒</span>
|
||||||
|
{t("resourceSections.products")}
|
||||||
|
{data.products.length > 0 && (
|
||||||
|
<span className="text-sm font-normal text-slate-500 dark:text-slate-400">
|
||||||
|
({t("resourceSections.totalProducts").replace("{count}", String(data.products.length))})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-2 gap-3 gap-y-4 sm:gap-4 md:grid-cols-8"
|
||||||
|
>
|
||||||
|
{paginatedProducts.map((product, i) => (
|
||||||
|
<button
|
||||||
|
key={`product-${productsPage}-${i}`}
|
||||||
|
type="button"
|
||||||
|
onClick={showUpdating}
|
||||||
|
className="group flex cursor-not-allowed flex-col text-left"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="overflow-hidden rounded-xl border border-slate-200 bg-white p-3 shadow-sm transition-all group-hover:shadow-md dark:border-slate-700 dark:bg-slate-900"
|
||||||
|
style={{ aspectRatio: "1/1" }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="h-full w-full object-contain transition-transform duration-300 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 line-clamp-2 text-center text-xs font-medium text-slate-700 dark:text-slate-300 sm:text-sm">
|
||||||
|
{product.name}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{productsTotalPages > 1 && (
|
||||||
|
<div className="mt-6 flex items-center justify-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setProductsPage((p) => Math.max(0, p - 1))}
|
||||||
|
disabled={productsPage === 0}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
|
||||||
|
aria-label="上一页"
|
||||||
|
>
|
||||||
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span className="px-4 text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
{productsPage + 1} / {productsTotalPages}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setProductsPage((p) => Math.min(productsTotalPages - 1, p + 1))}
|
||||||
|
disabled={productsPage === productsTotalPages - 1}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 transition-colors hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
|
||||||
|
aria-label="下一页"
|
||||||
|
>
|
||||||
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 网盘 - 分类 Tab */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-4 flex items-center gap-2 text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||||
|
<span>☁️</span>
|
||||||
|
{t("resourceSections.pan")}
|
||||||
|
</h3>
|
||||||
|
<div className="rounded-2xl border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
|
||||||
|
<div className="flex flex-wrap gap-1 border-b border-slate-200 p-2 dark:border-slate-700">
|
||||||
|
{data.pan.categories.map((cat) => (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActivePanCategory(cat.id)}
|
||||||
|
className={`rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
|
||||||
|
activePanCategory === cat.id
|
||||||
|
? "bg-sky-100 text-sky-700 dark:bg-sky-900/50 dark:text-sky-300"
|
||||||
|
: "text-slate-600 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="mr-1.5">{cat.icon}</span>
|
||||||
|
{cat.title}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
{activeCategory && (
|
||||||
|
<div
|
||||||
|
className="grid gap-3"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 260px), 1fr))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeCategory.items.map((item, i) => (
|
||||||
|
<a
|
||||||
|
key={i}
|
||||||
|
href={item.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer sponsored"
|
||||||
|
className="flex items-center gap-3 rounded-xl border border-slate-100 p-3 transition-colors hover:border-sky-200 hover:bg-sky-50/50 dark:border-slate-700 dark:hover:border-sky-800 dark:hover:bg-sky-900/20"
|
||||||
|
>
|
||||||
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-slate-100 text-lg dark:bg-slate-800">
|
||||||
|
{activeCategory.icon}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="font-medium text-slate-900 dark:text-slate-100">{item.name}</div>
|
||||||
|
<div className="text-xs text-slate-500 dark:text-slate-400">{item.desc}</div>
|
||||||
|
</div>
|
||||||
|
<span className="shrink-0 text-sm font-medium text-sky-600">→</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{toast && (
|
||||||
|
<div
|
||||||
|
className="fixed bottom-8 left-1/2 z-50 -translate-x-1/2 rounded-lg bg-slate-800 px-6 py-3 text-sm font-medium text-white shadow-lg dark:bg-slate-700"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{t("updating")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ const resources = [
|
|||||||
lang: "中文",
|
lang: "中文",
|
||||||
tag: "📱 社区",
|
tag: "📱 社区",
|
||||||
title: "数字游民部落 — 中文社区指南",
|
title: "数字游民部落 — 中文社区指南",
|
||||||
desc: "连接全球华人数字游民:经验分享、城市攻略、合租搭伴、线下 Meetup",
|
desc: "连接全球数字游民:经验分享、城市攻略、合租搭伴、线下 Meetup",
|
||||||
source: "微信公众号",
|
source: "微信公众号",
|
||||||
color: "bg-cyan-50 text-cyan-700 border-cyan-200",
|
color: "bg-cyan-50 text-cyan-700 border-cyan-200",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { Link, useTranslation } from "@/i18n/navigation";
|
import { Link, useTranslation } from "@/i18n/navigation";
|
||||||
|
|
||||||
const days = [
|
const days = [
|
||||||
@@ -14,6 +15,7 @@ const days = [
|
|||||||
|
|
||||||
export default function Roadmap() {
|
export default function Roadmap() {
|
||||||
const { t } = useTranslation("roadmap");
|
const { t } = useTranslation("roadmap");
|
||||||
|
const [courseToast, setCourseToast] = useState(false);
|
||||||
return (
|
return (
|
||||||
<section id="roadmap" className="bg-slate-50 py-20 sm:py-28 dark:bg-slate-900">
|
<section id="roadmap" className="bg-slate-50 py-20 sm:py-28 dark:bg-slate-900">
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
@@ -26,70 +28,99 @@ export default function Roadmap() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-14 grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
{/* 7天入门文档 - 独立区块 */}
|
||||||
{days.map((d) => (
|
<div className="mt-14">
|
||||||
<Link
|
<h3 className="mb-4 text-center text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
|
||||||
key={d.day}
|
{t("daysSection")}
|
||||||
href={`/day/${d.day}`}
|
</h3>
|
||||||
className="card-hover group flex flex-col rounded-2xl border border-slate-100 bg-white p-5 shadow-sm transition-all hover:border-sky-200 hover:shadow-md dark:border-slate-800 dark:bg-slate-800 dark:hover:border-sky-700"
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
>
|
{days.map((d) => (
|
||||||
<div className="flex items-center gap-3">
|
<Link
|
||||||
<div
|
key={d.day}
|
||||||
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-xs font-bold text-white ${d.color}`}
|
href={`/day/${d.day}`}
|
||||||
>
|
className="card-hover group flex flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition-all hover:border-sky-200 hover:shadow-md dark:border-slate-700 dark:bg-slate-800 dark:hover:border-sky-600"
|
||||||
DAY {d.day}
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-xs font-bold text-white ${d.color}`}
|
||||||
|
>
|
||||||
|
DAY {d.day}
|
||||||
|
</div>
|
||||||
|
<span className="text-xl">{d.icon}</span>
|
||||||
|
<h3 className="text-base font-bold text-slate-900 group-hover:text-sky-600 dark:text-slate-100 dark:group-hover:text-sky-400">
|
||||||
|
{t(`days.${d.day}.title`)}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xl">{d.icon}</span>
|
<p className="mt-3 flex-1 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
|
||||||
<h3 className="text-base font-bold text-slate-900 group-hover:text-sky-600 dark:text-slate-100 dark:group-hover:text-sky-400">
|
{t(`days.${d.day}.desc`)}
|
||||||
{t(`days.${d.day}.title`)}
|
</p>
|
||||||
</h3>
|
<span className="mt-3 inline-flex items-center text-sm font-medium text-sky-600 dark:text-sky-400">
|
||||||
</div>
|
{t("viewDetails")}
|
||||||
<p className="mt-3 flex-1 text-sm leading-relaxed text-slate-600 dark:text-slate-400">
|
</span>
|
||||||
{t(`days.${d.day}.desc`)}
|
</Link>
|
||||||
</p>
|
))}
|
||||||
<span className="mt-3 inline-flex items-center text-sm font-medium text-sky-600 dark:text-sky-400">
|
</div>
|
||||||
{t("viewDetails")}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
<Link
|
|
||||||
href="/ebook"
|
|
||||||
className="card-hover group flex flex-col rounded-2xl border border-slate-100 bg-white p-5 shadow-sm transition-all hover:border-sky-200 hover:shadow-md dark:border-slate-800 dark:bg-slate-800 dark:hover:border-sky-700"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-gradient-to-r from-sky-500 to-cyan-500 text-xs font-bold text-white">
|
|
||||||
📖
|
|
||||||
</div>
|
|
||||||
<h3 className="text-base font-bold text-slate-900 group-hover:text-sky-600">
|
|
||||||
{t("ebook.title")}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<p className="mt-3 flex-1 text-sm leading-relaxed text-slate-600">
|
|
||||||
{t("ebook.desc")}
|
|
||||||
</p>
|
|
||||||
<span className="mt-3 inline-flex items-center text-sm font-medium text-sky-600">
|
|
||||||
{t("ebook.cta")}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-16 overflow-hidden rounded-2xl border border-amber-200 bg-gradient-to-r from-amber-50 to-orange-50 p-8 text-center dark:border-amber-800 dark:from-amber-900/30 dark:to-orange-900/30 sm:p-10">
|
{/* 电子书 + 实战课 - 平级展示 */}
|
||||||
<div className="mb-2 text-sm font-semibold text-amber-600 dark:text-amber-400">
|
<div className="mt-16">
|
||||||
{t("course.tag")}
|
<h3 className="mb-6 text-center text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
|
||||||
</div>
|
{t("productsSection")}
|
||||||
<h3 className="text-2xl font-bold text-slate-900">
|
|
||||||
{t("course.title")}
|
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mx-auto mt-3 max-w-xl text-slate-600 dark:text-slate-400">
|
<div className="grid gap-6 sm:grid-cols-2">
|
||||||
{t("course.desc")}
|
<Link
|
||||||
</p>
|
href="/ebook"
|
||||||
<Link
|
className="group flex flex-col overflow-hidden rounded-2xl border-2 border-slate-200 bg-gradient-to-br from-sky-50 to-cyan-50/50 p-8 shadow-md transition-all hover:border-sky-300 hover:shadow-lg dark:border-slate-700 dark:from-sky-900/20 dark:to-cyan-900/10 dark:hover:border-sky-600"
|
||||||
href="/course"
|
>
|
||||||
className="mt-6 inline-flex items-center gap-2 rounded-full bg-amber-500 px-8 py-3 font-semibold text-white transition-colors hover:bg-amber-600 dark:bg-amber-600 dark:hover:bg-amber-500"
|
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-r from-sky-500 to-cyan-500 text-3xl shadow-lg shadow-sky-200/50 dark:shadow-sky-900/30">
|
||||||
>
|
📖
|
||||||
{t("course.cta")}
|
</div>
|
||||||
</Link>
|
<h3 className="text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||||
|
{t("ebook.title")}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 flex-1 text-slate-600 dark:text-slate-400">
|
||||||
|
{t("ebook.desc")}
|
||||||
|
</p>
|
||||||
|
<span className="mt-6 inline-flex items-center gap-2 text-sm font-semibold text-sky-600 dark:text-sky-400">
|
||||||
|
{t("ebook.cta")}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex flex-col overflow-hidden rounded-2xl border-2 border-amber-200 bg-gradient-to-br from-amber-50 to-orange-50/50 p-8 shadow-md transition-all dark:border-amber-800 dark:from-amber-900/20 dark:to-orange-900/10">
|
||||||
|
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-r from-amber-500 to-orange-500 text-3xl shadow-lg shadow-amber-200/50 dark:shadow-amber-900/30">
|
||||||
|
🎓
|
||||||
|
</div>
|
||||||
|
<span className="mb-2 inline-block text-xs font-semibold uppercase tracking-wider text-amber-600 dark:text-amber-400">
|
||||||
|
{t("course.tag")}
|
||||||
|
</span>
|
||||||
|
<h3 className="text-xl font-bold text-slate-900 dark:text-slate-100">
|
||||||
|
{t("course.title")}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 flex-1 text-slate-600 dark:text-slate-400">
|
||||||
|
{t("course.desc")}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setCourseToast(true);
|
||||||
|
setTimeout(() => setCourseToast(false), 2000);
|
||||||
|
}}
|
||||||
|
className="mt-6 inline-flex cursor-not-allowed items-center gap-2 self-start rounded-full bg-amber-500 px-6 py-3 font-semibold text-white opacity-90 transition hover:opacity-100 dark:bg-amber-600"
|
||||||
|
>
|
||||||
|
{t("course.cta")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{courseToast && (
|
||||||
|
<div
|
||||||
|
className="fixed bottom-8 left-1/2 z-50 -translate-x-1/2 rounded-lg bg-slate-800 px-6 py-3 text-sm font-medium text-white shadow-lg dark:bg-slate-700"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{t("course.updating")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,13 +10,6 @@ type ToolsProps = {
|
|||||||
export default function Tools({ data }: ToolsProps) {
|
export default function Tools({ data }: ToolsProps) {
|
||||||
const categories = data.categories;
|
const categories = data.categories;
|
||||||
const { t } = useTranslation("tools");
|
const { t } = useTranslation("tools");
|
||||||
const total = categories.reduce((s, c) => s + c.tools.length, 0);
|
|
||||||
const toolStats = [
|
|
||||||
{ value: `${total}+`, key: "tools" as const },
|
|
||||||
{ value: String(categories.length), key: "categories" as const },
|
|
||||||
{ value: "数字游民", key: "nomad" as const },
|
|
||||||
{ value: "佣金", key: "affiliate" as const },
|
|
||||||
];
|
|
||||||
return (
|
return (
|
||||||
<section id="tools" className="py-20 sm:py-28 dark:bg-slate-950">
|
<section id="tools" className="py-20 sm:py-28 dark:bg-slate-950">
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
@@ -73,20 +66,6 @@ export default function Tools({ data }: ToolsProps) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-14 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
|
||||||
{toolStats.map((s) => (
|
|
||||||
<div
|
|
||||||
key={s.key}
|
|
||||||
className="rounded-2xl border border-slate-100 bg-white p-5 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
|
||||||
>
|
|
||||||
<div className="text-3xl font-extrabold text-sky-600 dark:text-sky-400">
|
|
||||||
{s.value}
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-sm text-slate-500 dark:text-slate-400">{t(`stats.${s.key}`)}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Link } from "@/i18n/navigation";
|
import { Link } from "@/i18n/navigation";
|
||||||
|
import { useTranslation } from "@/i18n/navigation";
|
||||||
import { CTA_CONFIG } from "@/config/course";
|
import { CTA_CONFIG } from "@/config/course";
|
||||||
import { isPaid } from "@/app/lib/course-payment";
|
import { isPaid } from "@/app/lib/course-payment";
|
||||||
|
|
||||||
export function CTASection() {
|
export function CTASection() {
|
||||||
|
const { t } = useTranslation("community");
|
||||||
const [paid, setPaid] = useState(false);
|
const [paid, setPaid] = useState(false);
|
||||||
|
const [toast, setToast] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPaid(isPaid());
|
setPaid(isPaid());
|
||||||
const onPayUpdate = () => setPaid(isPaid());
|
const onPayUpdate = () => setPaid(isPaid());
|
||||||
@@ -34,13 +37,34 @@ export function CTASection() {
|
|||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
{paid ? (
|
||||||
href={paid ? "/course/0/0" : "/join"}
|
<Link
|
||||||
className="inline-flex items-center gap-2 rounded-full bg-[var(--accent)] px-8 py-4 text-lg font-semibold text-[var(--accent-foreground)] shadow-lg shadow-[var(--accent)]/25 transition-all duration-300 hover:scale-105 hover:opacity-95 hover:shadow-xl hover:shadow-[var(--accent)]/30 sm:px-10"
|
href="/course/0/0"
|
||||||
>
|
className="inline-flex items-center gap-2 rounded-full bg-[var(--accent)] px-8 py-4 text-lg font-semibold text-[var(--accent-foreground)] shadow-lg shadow-[var(--accent)]/25 transition-all duration-300 hover:scale-105 hover:opacity-95 hover:shadow-xl hover:shadow-[var(--accent)]/30 sm:px-10"
|
||||||
{paid ? "开始学习 →" : "立即加入 →"}
|
>
|
||||||
</Link>
|
开始学习 →
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setToast(true);
|
||||||
|
setTimeout(() => setToast(false), 2000);
|
||||||
|
}}
|
||||||
|
className="inline-flex cursor-not-allowed items-center gap-2 rounded-full bg-[var(--accent)] px-8 py-4 text-lg font-semibold text-[var(--accent-foreground)] opacity-90 shadow-lg shadow-[var(--accent)]/25 transition hover:opacity-100 sm:px-10"
|
||||||
|
>
|
||||||
|
立即加入 →
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{toast && (
|
||||||
|
<div
|
||||||
|
className="fixed bottom-8 left-1/2 z-50 -translate-x-1/2 rounded-lg bg-slate-800 px-6 py-3 text-sm font-medium text-white shadow-lg dark:bg-slate-700"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{t("updating")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Link } from "@/i18n/navigation";
|
import { Link } from "@/i18n/navigation";
|
||||||
|
import { useTranslation } from "@/i18n/navigation";
|
||||||
import { HERO_CONFIG, CURRICULUM_CONFIG } from "@/config/course";
|
import { HERO_CONFIG, CURRICULUM_CONFIG } from "@/config/course";
|
||||||
import { isPaid } from "@/app/lib/course-payment";
|
import { isPaid } from "@/app/lib/course-payment";
|
||||||
import { getLastWatched, getProgress } from "@/app/lib/learning";
|
import { getLastWatched, getProgress } from "@/app/lib/learning";
|
||||||
@@ -9,7 +10,9 @@ import { getLastWatched, getProgress } from "@/app/lib/learning";
|
|||||||
const TOTAL_LESSONS = CURRICULUM_CONFIG.modules.reduce((s, m) => s + m.lessons.length, 0);
|
const TOTAL_LESSONS = CURRICULUM_CONFIG.modules.reduce((s, m) => s + m.lessons.length, 0);
|
||||||
|
|
||||||
export function CourseHero() {
|
export function CourseHero() {
|
||||||
|
const { t } = useTranslation("community");
|
||||||
const [paid, setPaid] = useState(false);
|
const [paid, setPaid] = useState(false);
|
||||||
|
const [toast, setToast] = useState(false);
|
||||||
const [last, setLast] = useState<ReturnType<typeof getLastWatched>>(null);
|
const [last, setLast] = useState<ReturnType<typeof getLastWatched>>(null);
|
||||||
const [progress, setProgress] = useState({ completed: 0, percent: 0 });
|
const [progress, setProgress] = useState({ completed: 0, percent: 0 });
|
||||||
|
|
||||||
@@ -60,13 +63,24 @@ export function CourseHero() {
|
|||||||
>
|
>
|
||||||
继续学习 →
|
继续学习 →
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : paid ? (
|
||||||
<Link
|
<Link
|
||||||
href={paid ? "/course/0/0" : "/join"}
|
href="/course/0/0"
|
||||||
className="inline-flex shrink-0 items-center gap-2 rounded-full bg-[var(--accent)] px-8 py-4 text-lg font-semibold text-[var(--accent-foreground)] transition hover:opacity-90 sm:px-10"
|
className="inline-flex shrink-0 items-center gap-2 rounded-full bg-[var(--accent)] px-8 py-4 text-lg font-semibold text-[var(--accent-foreground)] transition hover:opacity-90 sm:px-10"
|
||||||
>
|
>
|
||||||
{paid ? "进入课程 →" : "立即报名 →"}
|
进入课程 →
|
||||||
</Link>
|
</Link>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setToast(true);
|
||||||
|
setTimeout(() => setToast(false), 2000);
|
||||||
|
}}
|
||||||
|
className="inline-flex shrink-0 cursor-not-allowed items-center gap-2 rounded-full bg-[var(--accent)] px-8 py-4 text-lg font-semibold text-[var(--accent-foreground)] opacity-90 transition hover:opacity-100 sm:px-10"
|
||||||
|
>
|
||||||
|
立即报名 →
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{paid && progress.completed > 0 && (
|
{paid && progress.completed > 0 && (
|
||||||
@@ -84,6 +98,14 @@ export function CourseHero() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{toast && (
|
||||||
|
<div
|
||||||
|
className="fixed bottom-8 left-1/2 z-50 -translate-x-1/2 rounded-lg bg-slate-800 px-6 py-3 text-sm font-medium text-white shadow-lg dark:bg-slate-700"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{t("updating")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,5 +82,49 @@ async function getThemeToolsDataFallback(): Promise<{
|
|||||||
return mod.default;
|
return mod.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ResourceData = {
|
||||||
|
books: Array<{ cover: string; title: string; href: string }>;
|
||||||
|
products: Array<{ image: string; name: string; href: string }>;
|
||||||
|
pan: {
|
||||||
|
categories: Array<{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
items: Array<{ name: string; desc: string; href: string }>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 加载资源导航数据:图书、商业数码、网盘 */
|
||||||
|
export async function getResourceData(): Promise<ResourceData> {
|
||||||
|
const theme = CURRENT_THEME;
|
||||||
|
if (theme === "digital-nomad") {
|
||||||
|
const [booksMod, productsMod, panMod] = await Promise.all([
|
||||||
|
import("@/content/themes/digital-nomad/data/books.json"),
|
||||||
|
import("@/content/themes/digital-nomad/data/products.json"),
|
||||||
|
import("@/content/themes/digital-nomad/data/pan.json"),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
books: booksMod.default.books,
|
||||||
|
products: productsMod.default.products,
|
||||||
|
pan: panMod.default,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return getResourceDataFallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getResourceDataFallback(): Promise<ResourceData> {
|
||||||
|
const [booksMod, productsMod, panMod] = await Promise.all([
|
||||||
|
import("@/content/themes/digital-nomad/data/books.json"),
|
||||||
|
import("@/content/themes/digital-nomad/data/products.json"),
|
||||||
|
import("@/content/themes/digital-nomad/data/pan.json"),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
books: booksMod.default.books,
|
||||||
|
products: productsMod.default.products,
|
||||||
|
pan: panMod.default,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export { CURRENT_THEME, THEME_IDS };
|
export { CURRENT_THEME, THEME_IDS };
|
||||||
export type { ThemeId };
|
export type { ThemeId };
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ export const THEME_CONFIGS: Record<ThemeId, ThemeConfig> = {
|
|||||||
id: "digital-nomad",
|
id: "digital-nomad",
|
||||||
name: "数字游民指南",
|
name: "数字游民指南",
|
||||||
nameEn: "Digital Nomad Guide",
|
nameEn: "Digital Nomad Guide",
|
||||||
tagline: "开源免费的数字游民资源平台",
|
tagline: "数字游民资源平台",
|
||||||
taglineEn: "Open-source digital nomad resource platform",
|
taglineEn: "Digital nomad resource platform",
|
||||||
meta: {
|
meta: {
|
||||||
title: "数字游民指南 | Digital Nomad Guide",
|
title: "数字游民指南 | Digital Nomad Guide",
|
||||||
description:
|
description:
|
||||||
|
|||||||
19
content/themes/digital-nomad/data/books.json
Normal file
19
content/themes/digital-nomad/data/books.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"books": [
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/6679955-M.jpg", "title": "一人企业方法论", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/1252736-M.jpg", "title": "数字游民", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/10961054-M.jpg", "title": "远程工作革命", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/539652-M.jpg", "title": "4小时工作周", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/10449931-M.jpg", "title": "纳瓦尔宝典", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/8337563-M.jpg", "title": "穷查理宝典", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/7104760-M.jpg", "title": "精益创业", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/9002334-M.jpg", "title": "从0到1", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/388913-M.jpg", "title": "黑客与画家", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/6679955-M.jpg", "title": "重来", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/7988607-M.jpg", "title": "深度工作", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/10467082-M.jpg", "title": "刻意练习", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/8315355-M.jpg", "title": "原则", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/13290711-M.jpg", "title": "思考快与慢", "href": "https://www.jd.com" },
|
||||||
|
{ "cover": "https://covers.openlibrary.org/b/id/431011-M.jpg", "title": "影响力", "href": "https://www.jd.com" }
|
||||||
|
]
|
||||||
|
}
|
||||||
52
content/themes/digital-nomad/data/pan.json
Normal file
52
content/themes/digital-nomad/data/pan.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"id": "docs",
|
||||||
|
"title": "文档",
|
||||||
|
"icon": "📄",
|
||||||
|
"items": [
|
||||||
|
{ "name": "飞书文档", "desc": "在线协作", "href": "https://www.feishu.cn" },
|
||||||
|
{ "name": "Notion", "desc": "知识库", "href": "https://www.notion.so" },
|
||||||
|
{ "name": "语雀", "desc": "文档协作", "href": "https://www.yuque.com" },
|
||||||
|
{ "name": "腾讯文档", "desc": "在线协作", "href": "https://docs.qq.com" },
|
||||||
|
{ "name": "石墨文档", "desc": "在线表格", "href": "https://shimo.im" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "templates",
|
||||||
|
"title": "模版",
|
||||||
|
"icon": "📋",
|
||||||
|
"items": [
|
||||||
|
{ "name": "飞书模版", "desc": "文档/表格模版", "href": "https://www.feishu.cn" },
|
||||||
|
{ "name": "Notion模版", "desc": "知识库模版", "href": "https://www.notion.so" },
|
||||||
|
{ "name": "Canva", "desc": "设计模版", "href": "https://www.canva.com" },
|
||||||
|
{ "name": "Figma", "desc": "UI模版", "href": "https://www.figma.com" },
|
||||||
|
{ "name": "PPT模版", "desc": "演示文稿", "href": "https://www.jd.com" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firmware",
|
||||||
|
"title": "系统固件",
|
||||||
|
"icon": "⚙️",
|
||||||
|
"items": [
|
||||||
|
{ "name": "OpenWrt", "desc": "路由器固件", "href": "https://openwrt.org" },
|
||||||
|
{ "name": "Armbian", "desc": "ARM开发板", "href": "https://www.armbian.com" },
|
||||||
|
{ "name": "树莓派OS", "desc": "Raspberry Pi", "href": "https://www.raspberrypi.com" },
|
||||||
|
{ "name": "Android TV", "desc": "电视盒子固件", "href": "https://source.android.com" },
|
||||||
|
{ "name": "固件仓库", "desc": "固件合集", "href": "https://www.jd.com" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "storage",
|
||||||
|
"title": "网盘",
|
||||||
|
"icon": "☁️",
|
||||||
|
"items": [
|
||||||
|
{ "name": "阿里云盘", "desc": "大容量不限速", "href": "https://www.aliyundrive.com" },
|
||||||
|
{ "name": "百度网盘", "desc": "资源丰富", "href": "https://pan.baidu.com" },
|
||||||
|
{ "name": "夸克网盘", "desc": "高速下载", "href": "https://pan.quark.cn" },
|
||||||
|
{ "name": "115网盘", "desc": "离线下载", "href": "https://115.com" },
|
||||||
|
{ "name": "OneDrive", "desc": "微软云盘", "href": "https://onedrive.live.com" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
19
content/themes/digital-nomad/data/products.json
Normal file
19
content/themes/digital-nomad/data/products.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"products": [
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1517336714731-489689fd1ca8?w=400", "name": "MacBook Pro", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1544244015-0df4b3ffc2b0?w=400", "name": "iPad Pro", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400", "name": "Kindle", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1587829741301-dc798b83add3?w=400", "name": "罗技MX Keys", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1527864550417-7fd91fc51a46?w=400", "name": "罗技MX Master", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=400", "name": "降噪耳机", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?w=400", "name": "便携显示器", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1597872200969-2b65d565bd41?w=400", "name": "移动硬盘", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1583394838336-acd977736f90?w=400", "name": "氮化镓充电器", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1593642632559-0c6d3fc62b89?w=400", "name": "拓展坞", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1574717024653-61fd2cf4d44d?w=400", "name": "云台稳定器", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1593640408182-31c70c8268f5?w=400", "name": "补光灯", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1590602847861-f357a9332bbc?w=400", "name": "麦克风", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1610557892470-55d9e80c0bce?w=400", "name": "树莓派", "href": "https://www.jd.com" },
|
||||||
|
{ "image": "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400", "name": "开发板", "href": "https://www.jd.com" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -15,11 +15,11 @@
|
|||||||
"resources": "Resources"
|
"resources": "Resources"
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "Open source · 200+ curated resources",
|
"badge": "Curated · 200+ resources",
|
||||||
"title": "Digital Nomad",
|
"title": "Digital Nomad",
|
||||||
"titleSuffix": "Life Guide",
|
"titleSuffix": "Life Guide",
|
||||||
"subtitle": "Start your digital nomad journey in 7 days from scratch",
|
"subtitle": "Start your digital nomad journey in 7 days from scratch",
|
||||||
"subtitleEn": "The open-source guide to becoming a digital nomad",
|
"subtitleEn": "The guide to becoming a digital nomad",
|
||||||
"ctaStart": "🚀 Start Learning",
|
"ctaStart": "🚀 Start Learning",
|
||||||
"ctaResources": "📚 Browse Resources",
|
"ctaResources": "📚 Browse Resources",
|
||||||
"stats": {
|
"stats": {
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"path": "Learning Path",
|
"path": "Learning Path",
|
||||||
"tools": "Remote Tools",
|
"tools": "Remote Tools",
|
||||||
"destinations": "Destinations",
|
"destinations": "Destinations",
|
||||||
"free": "Free & Open"
|
"free": "Curated"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"subtitle2": "It lets you explore the world while creating value.",
|
"subtitle2": "It lets you explore the world while creating value.",
|
||||||
"stat": "Over",
|
"stat": "Over",
|
||||||
"statCount": "35 million",
|
"statCount": "35 million",
|
||||||
"statSuffix": "digital nomads worldwide",
|
"statSuffix": "digital nomads",
|
||||||
"items": {
|
"items": {
|
||||||
"location": {
|
"location": {
|
||||||
"title": "Location Freedom",
|
"title": "Location Freedom",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
},
|
},
|
||||||
"skill": {
|
"skill": {
|
||||||
"title": "Monetize Skills",
|
"title": "Monetize Skills",
|
||||||
"desc": "Programming, design, writing, marketing, consulting… Exchange your expertise for global income and build a diversified revenue stream."
|
"desc": "Programming, design, writing, marketing, consulting… Exchange your expertise for diversified income and build a diversified revenue stream."
|
||||||
},
|
},
|
||||||
"balance": {
|
"balance": {
|
||||||
"title": "Work-Life Balance",
|
"title": "Work-Life Balance",
|
||||||
@@ -59,6 +59,8 @@
|
|||||||
"titleHighlight": "Learning Path",
|
"titleHighlight": "Learning Path",
|
||||||
"subtitle": "One theme per day, from basics to launch—start your digital nomad journey step by step.",
|
"subtitle": "One theme per day, from basics to launch—start your digital nomad journey step by step.",
|
||||||
"viewDetails": "View details →",
|
"viewDetails": "View details →",
|
||||||
|
"daysSection": "7-Day Guide",
|
||||||
|
"productsSection": "Ebook · Bootcamp",
|
||||||
"days": {
|
"days": {
|
||||||
"1": { "title": "Meet Digital Nomads", "desc": "Understand what digital nomadism really means, assess if it fits you, and debunk common myths." },
|
"1": { "title": "Meet Digital Nomads", "desc": "Understand what digital nomadism really means, assess if it fits you, and debunk common myths." },
|
||||||
"2": { "title": "Discover Remote Skills", "desc": "Master the most in-demand remote skills—from coding and design to content creation—and find your direction." },
|
"2": { "title": "Discover Remote Skills", "desc": "Master the most in-demand remote skills—from coding and design to content creation—and find your direction." },
|
||||||
@@ -93,19 +95,28 @@
|
|||||||
},
|
},
|
||||||
"community": {
|
"community": {
|
||||||
"nomadCommunity": "Digital Nomad Community",
|
"nomadCommunity": "Digital Nomad Community",
|
||||||
"joinTitle": "Join the Global Chinese Digital Nomad Network",
|
"joinTitle": "Join the Digital Nomad Network",
|
||||||
"joinDesc": "Apply to join · 1000+ nomads · Weekly online shares · City guides",
|
"joinDesc": "Apply to join · 1000+ nomads · Weekly online shares · City guides",
|
||||||
"applyForm": "Apply via form",
|
"applyForm": "Apply via form",
|
||||||
"approveJoin": "Join after approval",
|
"approveJoin": "Join after approval",
|
||||||
"applyNow": "Apply now →",
|
"applyNow": "Enter private community →",
|
||||||
"applyHint": "Simple form, quick review",
|
"applyHint": "Private community entry",
|
||||||
"resourcesTitle": "Resource Navigation",
|
"resourcesTitle": "Resource Navigation",
|
||||||
|
"resourceSections": {
|
||||||
|
"books": "Books",
|
||||||
|
"products": "Products",
|
||||||
|
"pan": "Cloud & Resources",
|
||||||
|
"totalBooks": "{count} books",
|
||||||
|
"totalProducts": "{count} items"
|
||||||
|
},
|
||||||
"topicTag": "Topic",
|
"topicTag": "Topic",
|
||||||
"contributeSubtitle": "Digital Nomad Guide is open source, building the best Chinese digital nomad resource hub.",
|
"updating": "Coming soon",
|
||||||
|
"contributeSubtitle": "Digital Nomad Guide builds the best Chinese digital nomad resource hub.",
|
||||||
"contributeSubtitle2": "Welcome to add resources, improve content, and share experience.",
|
"contributeSubtitle2": "Welcome to add resources, improve content, and share experience.",
|
||||||
"links": {
|
"links": {
|
||||||
"github": { "title": "GitHub", "desc": "Open source—Star and PR welcome", "label": "View repo ↗" },
|
"github": { "title": "GitHub", "desc": "Open source—Star and PR welcome", "label": "View repo ↗" },
|
||||||
"destinations": { "title": "Destinations", "desc": "Global digital nomad city info", "label": "Explore cities ↗" },
|
"destinations": { "title": "Destinations", "desc": "Digital nomad city info", "label": "Explore cities ↗" },
|
||||||
|
"vipEntry": { "title": "异度星球", "desc": "Member-exclusive resources and community", "label": "Enter ↗" },
|
||||||
"indieDev": { "title": "Indie Dev", "desc": "From idea to product, indie developer guide", "label": "View guide ↗" },
|
"indieDev": { "title": "Indie Dev", "desc": "From idea to product, indie developer guide", "label": "View guide ↗" },
|
||||||
"aiAgent": { "title": "AI Agent", "desc": "AIGC & agents, Vibe Coding for creators", "label": "Learn more ↗" },
|
"aiAgent": { "title": "AI Agent", "desc": "AIGC & agents, Vibe Coding for creators", "label": "Learn more ↗" },
|
||||||
"cloudPhone": { "title": "Cloud Phone", "desc": "Low-cost self-hosted cloud phone for TikTok/YouTube", "label": "Setup guide ↗" },
|
"cloudPhone": { "title": "Cloud Phone", "desc": "Low-cost self-hosted cloud phone for TikTok/YouTube", "label": "Setup guide ↗" },
|
||||||
@@ -119,7 +130,7 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"siteName": "Digital Nomad Guide",
|
"siteName": "Digital Nomad Guide",
|
||||||
"tagline": "Open-source digital nomad resource platform",
|
"tagline": "Digital nomad resource platform",
|
||||||
"tagline2": "Start your free work and travel lifestyle",
|
"tagline2": "Start your free work and travel lifestyle",
|
||||||
"sections": {
|
"sections": {
|
||||||
"guide": "Guide",
|
"guide": "Guide",
|
||||||
@@ -145,7 +156,7 @@
|
|||||||
"contact": "Contact"
|
"contact": "Contact"
|
||||||
},
|
},
|
||||||
"madeBy": "Made with 🌍 by Digital Nomad Community | DN Guide",
|
"madeBy": "Made with 🌍 by Digital Nomad Community | DN Guide",
|
||||||
"openSource": "Open source · Community driven"
|
"openSource": "Curated content · Regularly updated"
|
||||||
},
|
},
|
||||||
"shop": {
|
"shop": {
|
||||||
"badge": "🛒 Curated Shop",
|
"badge": "🛒 Curated Shop",
|
||||||
|
|||||||
@@ -15,11 +15,11 @@
|
|||||||
"resources": "资源"
|
"resources": "资源"
|
||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "开源免费 · 收录 200+ 篇实用资源",
|
"badge": "精选资源 · 收录 200+ 篇实用内容",
|
||||||
"title": "数字游民",
|
"title": "数字游民",
|
||||||
"titleSuffix": "生活指南",
|
"titleSuffix": "生活指南",
|
||||||
"subtitle": "从零开始,7天开启你的数字游民生活",
|
"subtitle": "从零开始,7天开启你的数字游民生活",
|
||||||
"subtitleEn": "The open-source guide to becoming a digital nomad",
|
"subtitleEn": "The guide to becoming a digital nomad",
|
||||||
"ctaStart": "🚀 开始学习",
|
"ctaStart": "🚀 开始学习",
|
||||||
"ctaResources": "📚 浏览资源",
|
"ctaResources": "📚 浏览资源",
|
||||||
"stats": {
|
"stats": {
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"path": "入门路径",
|
"path": "入门路径",
|
||||||
"tools": "远程工具",
|
"tools": "远程工具",
|
||||||
"destinations": "目的地",
|
"destinations": "目的地",
|
||||||
"free": "免费开源"
|
"free": "精选资源"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"titleSuffix": "?",
|
"titleSuffix": "?",
|
||||||
"subtitle": "数字游民是一种利用互联网技术远程工作、不受地理位置限制的生活方式。",
|
"subtitle": "数字游民是一种利用互联网技术远程工作、不受地理位置限制的生活方式。",
|
||||||
"subtitle2": "它让你在探索世界的同时,持续创造价值。",
|
"subtitle2": "它让你在探索世界的同时,持续创造价值。",
|
||||||
"stat": "全球已有超过",
|
"stat": "已有超过",
|
||||||
"statCount": "3500 万",
|
"statCount": "3500 万",
|
||||||
"statSuffix": "数字游民",
|
"statSuffix": "数字游民",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
},
|
},
|
||||||
"skill": {
|
"skill": {
|
||||||
"title": "技能变现",
|
"title": "技能变现",
|
||||||
"desc": "编程、设计、写作、营销、咨询……用你的专业技能换取全球收入,构建多元化的收入体系。"
|
"desc": "编程、设计、写作、营销、咨询……用你的专业技能换取多元收入,构建多元化的收入体系。"
|
||||||
},
|
},
|
||||||
"balance": {
|
"balance": {
|
||||||
"title": "工作生活平衡",
|
"title": "工作生活平衡",
|
||||||
@@ -59,6 +59,8 @@
|
|||||||
"titleHighlight": "学习路径",
|
"titleHighlight": "学习路径",
|
||||||
"subtitle": "从入门到出发,每天一个主题,循序渐进开启你的数字游民之旅。",
|
"subtitle": "从入门到出发,每天一个主题,循序渐进开启你的数字游民之旅。",
|
||||||
"viewDetails": "查看详情 →",
|
"viewDetails": "查看详情 →",
|
||||||
|
"daysSection": "7天入门文档",
|
||||||
|
"productsSection": "电子书 · 实战课",
|
||||||
"days": {
|
"days": {
|
||||||
"1": { "title": "认识数字游民", "desc": "了解数字游民的真正含义,评估这种生活方式是否适合你,破除常见误解。" },
|
"1": { "title": "认识数字游民", "desc": "了解数字游民的真正含义,评估这种生活方式是否适合你,破除常见误解。" },
|
||||||
"2": { "title": "发现远程技能", "desc": "掌握最受欢迎的远程工作技能,从编程、设计到内容创作,找到你的方向。" },
|
"2": { "title": "发现远程技能", "desc": "掌握最受欢迎的远程工作技能,从编程、设计到内容创作,找到你的方向。" },
|
||||||
@@ -93,19 +95,28 @@
|
|||||||
},
|
},
|
||||||
"community": {
|
"community": {
|
||||||
"nomadCommunity": "数字游民社区",
|
"nomadCommunity": "数字游民社区",
|
||||||
"joinTitle": "加入全球华人数字游民网络",
|
"joinTitle": "加入数字游民网络",
|
||||||
"joinDesc": "填写申请表加入社区 · 1000+ 数字游民 · 每周线上分享 · 城市攻略",
|
"joinDesc": "填写申请表加入社区 · 1000+ 数字游民 · 每周线上分享 · 城市攻略",
|
||||||
"applyForm": "填写表单即可申请",
|
"applyForm": "填写表单即可申请",
|
||||||
"approveJoin": "审核通过入群",
|
"approveJoin": "审核通过入群",
|
||||||
"applyNow": "立即申请加入 →",
|
"applyNow": "进入私域 →",
|
||||||
"applyHint": "简单表单,快速审核",
|
"applyHint": "私域入口",
|
||||||
"resourcesTitle": "资源导航",
|
"resourcesTitle": "资源导航",
|
||||||
|
"resourceSections": {
|
||||||
|
"books": "精选图书",
|
||||||
|
"products": "商业数码",
|
||||||
|
"pan": "网盘与资源",
|
||||||
|
"totalBooks": "共 {count} 本",
|
||||||
|
"totalProducts": "共 {count} 件"
|
||||||
|
},
|
||||||
"topicTag": "专题",
|
"topicTag": "专题",
|
||||||
"contributeSubtitle": "数字游民指南是开源项目,致力于做最好的中文数字游民资源聚合站。",
|
"updating": "更新中",
|
||||||
|
"contributeSubtitle": "数字游民指南致力于做最好的中文数字游民资源聚合站。",
|
||||||
"contributeSubtitle2": "欢迎补充资源、改进内容、分享经验。",
|
"contributeSubtitle2": "欢迎补充资源、改进内容、分享经验。",
|
||||||
"links": {
|
"links": {
|
||||||
"github": { "title": "GitHub", "desc": "开源项目,欢迎 Star 和 PR", "label": "访问仓库 ↗" },
|
"github": { "title": "GitHub", "desc": "开源项目,欢迎 Star 和 PR", "label": "访问仓库 ↗" },
|
||||||
"destinations": { "title": "目的地数据库", "desc": "全球数字游民城市信息", "label": "探索城市 ↗" },
|
"destinations": { "title": "目的地数据库", "desc": "数字游民城市信息", "label": "探索城市 ↗" },
|
||||||
|
"vipEntry": { "title": "异度星球", "desc": "会员专属资源与社区", "label": "进入 ↗" },
|
||||||
"indieDev": { "title": "独立开发", "desc": "从想法到产品,独立开发者全流程指南", "label": "查看指南 ↗" },
|
"indieDev": { "title": "独立开发", "desc": "从想法到产品,独立开发者全流程指南", "label": "查看指南 ↗" },
|
||||||
"aiAgent": { "title": "AI Agent", "desc": "AIGC 与智能体,Vibe Coding 跨界创造", "label": "了解更多 ↗" },
|
"aiAgent": { "title": "AI Agent", "desc": "AIGC 与智能体,Vibe Coding 跨界创造", "label": "了解更多 ↗" },
|
||||||
"cloudPhone": { "title": "云手机", "desc": "低成本自建云手机,TikTok/YouTube 运营", "label": "搭建指南 ↗" },
|
"cloudPhone": { "title": "云手机", "desc": "低成本自建云手机,TikTok/YouTube 运营", "label": "搭建指南 ↗" },
|
||||||
@@ -119,7 +130,7 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"siteName": "数字游民指南",
|
"siteName": "数字游民指南",
|
||||||
"tagline": "开源免费的数字游民资源平台",
|
"tagline": "数字游民资源平台",
|
||||||
"tagline2": "助你开启自由工作与旅行的生活方式",
|
"tagline2": "助你开启自由工作与旅行的生活方式",
|
||||||
"sections": {
|
"sections": {
|
||||||
"guide": "指南",
|
"guide": "指南",
|
||||||
@@ -145,7 +156,7 @@
|
|||||||
"contact": "联系我们"
|
"contact": "联系我们"
|
||||||
},
|
},
|
||||||
"madeBy": "Made with 🌍 by 数字游民社区 | Digital Nomad Guide",
|
"madeBy": "Made with 🌍 by 数字游民社区 | Digital Nomad Guide",
|
||||||
"openSource": "开源共享 · 社区驱动"
|
"openSource": "精选内容 · 持续更新"
|
||||||
},
|
},
|
||||||
"shop": {
|
"shop": {
|
||||||
"badge": "🛒 精选好物",
|
"badge": "🛒 精选好物",
|
||||||
|
|||||||
Reference in New Issue
Block a user