Files
gitlab-instance-0a899031_di…/app/course/page.tsx
2026-03-07 09:34:01 -06:00

446 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
/* ─── data ─── */
const stats = [
{ value: "21", label: "节实战课程" },
{ value: "150+", label: "分钟视频" },
{ value: "3", label: "位实战讲师" },
];
const audiences = [
{
icon: "💼",
title: "职场人想转型",
desc: "受够了 996学会远程工作技能平滑过渡到自由生活方式",
},
{
icon: "👨‍💻",
title: "开发者 / 设计师",
desc: "你的技能天然适合远程,学会如何接全球客户、拿美金报酬",
},
{
icon: "📱",
title: "自媒体 / 内容创作者",
desc: "边旅行边创作,掌握数字游民内容变现的全套方法论",
},
{
icon: "🌍",
title: "想出国旅居的人",
desc: "签证、税务、保险、目的地一站搞定,少走弯路直接出发",
},
];
const benefits = [
{
num: 1,
title: "完整的远程工作体系",
desc: "从技能评估到客户获取,建立可持续的远程收入系统",
},
{
num: 2,
title: "打通 3+ 收入渠道",
desc: "远程全职、自由职业、数字产品、内容创作多管齐下",
},
{
num: 3,
title: "掌握 50+ 远程工具",
desc: "协作、会议、项目管理、财务,一个背包装下整个办公室",
},
{
num: 4,
title: "签证 + 税务 + 保险全通关",
desc: "55 国数字游民签证政策、合规税务方案、保险选购指南",
},
{
num: 5,
title: "跑通第一次数字游民旅程",
desc: "从选目的地到打包出发,手把手带你完成首次数字游民体验",
},
];
const curriculum = [
{
emoji: "🚀",
chapter: "基础篇 · 认识数字游民",
lessons: [
{ id: "01", title: "什么是数字游民?破除 5 个常见误解", duration: "12:30" },
{ id: "02", title: "数字游民三种模式:蜜蜂、乌龟、候鸟", duration: "08:45" },
],
},
{
emoji: "💡",
chapter: "技能篇 · 远程工作能力",
lessons: [
{ id: "03", title: "远程高薪技能全景图:找到你的方向", duration: "14:20" },
{ id: "04", title: "30天技能升级路线从评估到接单", duration: "11:15" },
{ id: "05", title: "建立个人品牌:让客户主动找你", duration: "09:40" },
],
},
{
emoji: "💰",
chapter: "收入篇 · 构建收入体系",
lessons: [
{ id: "06", title: "远程全职 vs 自由职业:选哪条路?", duration: "10:05" },
{ id: "07", title: "自由职业平台实操Upwork / Toptal / 电鸭", duration: "13:50" },
{ id: "08", title: "被动收入搭建:数字产品 & 在线课程", duration: "12:20" },
{ id: "09", title: "定价的艺术:如何报价不心虚", duration: "07:30" },
],
},
{
emoji: "🛠️",
chapter: "工具篇 · 移动办公装备",
lessons: [
{ id: "10", title: "硬件极简主义:一个背包装下办公室", duration: "06:45" },
{ id: "11", title: "远程协作工具栈Slack / Notion / Linear", duration: "08:30" },
{ id: "12", title: "安全必修课VPN / 密码管理 / 2FA", duration: "05:55" },
{ id: "13", title: "跨境财务工具Wise / Stripe / Revolut", duration: "07:10" },
],
},
{
emoji: "📋",
chapter: "合规篇 · 签证税务保险",
lessons: [
{ id: "14", title: "55 国数字游民签证政策全解读", duration: "15:30" },
{ id: "15", title: "税务居民身份与合规规划", duration: "11:40" },
{ id: "16", title: "数字游民保险选购指南", duration: "06:20" },
],
},
{
emoji: "🗺️",
chapter: "目的地篇 · 选择你的城市",
lessons: [
{ id: "17", title: "亚洲三城:清迈 / 巴厘岛 / 首尔深度对比", duration: "10:50" },
{ id: "18", title: "欧洲双雄:里斯本 / 巴塞罗那生活实录", duration: "09:25" },
{ id: "19", title: "美洲探索:墨西哥城 / 麦德林旅居指南", duration: "08:15" },
],
},
{
emoji: "🏆",
chapter: "实战篇 · 启程出发",
lessons: [
{ id: "20", title: "出发前检查清单 & 打包哲学", duration: "07:40" },
{ id: "21", title: "第一个月生存指南 & 长期可持续策略", duration: "13:10" },
],
},
];
const instructors = [
{
name: "Alex",
avatar: "🧑‍💻",
role: "全栈开发者 / 5年数字游民",
tag: "技术导师",
desc: "前字节跳动高级工程师2021年开始数字游民生活足迹遍布 20+ 国家,远程服务硅谷客户",
},
{
name: "Lisa",
avatar: "👩‍🎨",
role: "品牌设计师 / 自由职业教练",
tag: "变现专家",
desc: "Toptal 认证设计师,年收入 $120K+,帮助 500+ 人成功转型远程自由职业",
},
{
name: "Marco",
avatar: "🧳",
role: "旅居作家 / 签证顾问",
tag: "旅居达人",
desc: "常驻里斯本,持有 3 国数字游民签证,《数字游民签证手册》作者,深谙各国政策",
},
];
/* ─── component ─── */
export default function CoursePage() {
const [modalOpen, setModalOpen] = useState(false);
return (
<div className="min-h-screen bg-white text-slate-900">
{/* ── Nav ── */}
<header className="sticky top-0 z-50 border-b border-slate-100 bg-white/80 backdrop-blur-lg">
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-4 sm:px-6">
<a href="/" className="flex items-center gap-2 text-sm font-bold">
<span className="text-lg">🌍</span>
</a>
<button
onClick={() => setModalOpen(true)}
className="rounded-full bg-sky-500 px-4 py-1.5 text-sm font-semibold text-white transition-colors hover:bg-sky-600"
>
</button>
</div>
</header>
{/* ── Hero ── */}
<section className="relative overflow-hidden bg-gradient-to-b from-slate-50 to-white py-20 sm:py-28">
<div className="pointer-events-none absolute top-0 left-1/2 h-[500px] w-[700px] -translate-x-1/2 rounded-full bg-gradient-to-br from-sky-100/50 via-cyan-50/30 to-amber-50/20 blur-3xl" />
<div className="relative mx-auto max-w-3xl px-4 text-center sm:px-6">
<h1 className="text-4xl font-extrabold tracking-tight sm:text-5xl lg:text-6xl">
<span className="gradient-text"></span>
</h1>
<p className="mx-auto mt-5 max-w-xl text-lg leading-relaxed text-slate-600 sm:text-xl">
7
</p>
<div className="mt-10 flex items-center justify-center gap-6 sm:gap-10">
{stats.map((s) => (
<div key={s.label} className="text-center">
<div className="text-3xl font-extrabold text-sky-600 sm:text-4xl">
{s.value}
</div>
<div className="mt-1 text-sm text-slate-500">{s.label}</div>
</div>
))}
</div>
<button
onClick={() => setModalOpen(true)}
className="mt-10 inline-flex items-center gap-2 rounded-full bg-sky-500 px-10 py-4 text-lg font-semibold text-white shadow-lg shadow-sky-500/25 transition-all hover:bg-sky-600 hover:shadow-xl hover:shadow-sky-500/30"
>
<span></span>
</button>
</div>
</section>
{/* ── Audience ── */}
<section className="py-20 sm:py-24">
<div className="mx-auto max-w-5xl px-4 sm:px-6">
<h2 className="text-center text-2xl font-bold sm:text-3xl">
</h2>
<p className="mx-auto mt-3 max-w-xl text-center text-slate-500">
</p>
<div className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{audiences.map((a) => (
<div
key={a.title}
className="card-hover rounded-2xl border border-slate-100 bg-white p-6 text-center shadow-sm"
>
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-sky-50 text-3xl">
{a.icon}
</div>
<h3 className="mb-2 font-bold">{a.title}</h3>
<p className="text-sm leading-relaxed text-slate-500">
{a.desc}
</p>
</div>
))}
</div>
</div>
</section>
{/* ── Benefits ── */}
<section className="bg-slate-50 py-20 sm:py-24">
<div className="mx-auto max-w-5xl px-4 sm:px-6">
<h2 className="text-center text-2xl font-bold sm:text-3xl">
</h2>
<p className="mx-auto mt-3 max-w-xl text-center text-slate-500">
5
</p>
<div className="mt-14 space-y-5">
{benefits.map((b) => (
<div
key={b.num}
className="card-hover flex items-start gap-5 rounded-2xl border border-slate-100 bg-white p-6 shadow-sm sm:items-center"
>
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-sky-500 to-cyan-500 text-xl font-extrabold text-white shadow-md shadow-sky-500/20">
{b.num}
</div>
<div>
<h3 className="text-lg font-bold text-slate-900">{b.title}</h3>
<p className="mt-1 text-sm text-slate-500">{b.desc}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* ── Curriculum ── */}
<section className="py-20 sm:py-24">
<div className="mx-auto max-w-5xl px-4 sm:px-6">
<h2 className="text-center text-2xl font-bold sm:text-3xl">
21
</h2>
<p className="mx-auto mt-3 max-w-xl text-center text-slate-500">
</p>
<div className="mt-14 space-y-6">
{curriculum.map((ch) => (
<div
key={ch.chapter}
className="overflow-hidden rounded-2xl border border-slate-100 bg-white shadow-sm"
>
{/* Chapter header */}
<div className="flex items-center gap-3 border-b border-slate-100 bg-slate-50/80 px-6 py-4">
<span className="text-xl">{ch.emoji}</span>
<h3 className="font-bold text-slate-800">{ch.chapter}</h3>
</div>
{/* Lessons */}
<ul>
{ch.lessons.map((lesson, i) => (
<li
key={lesson.id}
className={`flex items-center justify-between px-6 py-3.5 transition-colors hover:bg-sky-50/40 ${
i < ch.lessons.length - 1
? "border-b border-slate-50"
: ""
}`}
>
<div className="flex items-center gap-3">
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-slate-100 text-xs font-bold text-slate-500">
{lesson.id}
</span>
<span className="text-sm font-medium text-slate-700 sm:text-base">
{lesson.title}
</span>
</div>
<span className="ml-4 shrink-0 text-xs tabular-nums text-slate-400">
{lesson.duration}
</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</section>
{/* ── Instructors ── */}
<section className="bg-slate-50 py-20 sm:py-24">
<div className="mx-auto max-w-5xl px-4 sm:px-6">
<h2 className="text-center text-2xl font-bold sm:text-3xl">
</h2>
<p className="mx-auto mt-3 max-w-xl text-center text-slate-500">
</p>
<div className="mt-14 grid gap-6 sm:grid-cols-3">
{instructors.map((ins) => (
<div
key={ins.name}
className="card-hover rounded-2xl border border-slate-100 bg-white p-6 text-center shadow-sm"
>
<div className="mx-auto mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-sky-100 to-cyan-50 text-5xl">
{ins.avatar}
</div>
<h3 className="text-lg font-bold">{ins.name}</h3>
<p className="mt-1 text-sm text-slate-500">{ins.role}</p>
<span className="mt-3 inline-block rounded-full bg-sky-50 px-3 py-1 text-xs font-semibold text-sky-600">
{ins.tag}
</span>
<p className="mt-4 text-sm leading-relaxed text-slate-500">
{ins.desc}
</p>
</div>
))}
</div>
</div>
</section>
{/* ── Final CTA ── */}
<section className="py-20 sm:py-28">
<div className="mx-auto max-w-3xl px-4 text-center sm:px-6">
<h2 className="text-2xl font-bold sm:text-3xl">
</h2>
<p className="mx-auto mt-4 max-w-lg text-slate-500">
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-4 text-sm text-slate-500">
<span className="rounded-full border border-slate-200 bg-white px-4 py-1.5">
21
</span>
<span className="rounded-full border border-slate-200 bg-white px-4 py-1.5">
</span>
<span className="rounded-full border border-slate-200 bg-white px-4 py-1.5">
</span>
</div>
<button
onClick={() => setModalOpen(true)}
className="mt-10 inline-flex items-center gap-2 rounded-full bg-sky-500 px-10 py-4 text-lg font-semibold text-white shadow-lg shadow-sky-500/25 transition-all hover:bg-sky-600 hover:shadow-xl hover:shadow-sky-500/30"
>
<span></span>
</button>
</div>
</section>
{/* ── Footer ── */}
<footer className="border-t border-slate-100 bg-white py-8 text-center text-sm text-slate-400">
<a href="/" className="font-medium text-slate-600 transition-colors hover:text-sky-600">
🌍
</a>
<span className="mx-2">·</span>
·
</footer>
{/* ── Modal ── */}
{modalOpen && (
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm"
onClick={() => setModalOpen(false)}
>
<div
className="relative w-full max-w-sm animate-fade-in rounded-2xl bg-white p-8 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => setModalOpen(false)}
className="absolute top-4 right-4 flex h-8 w-8 items-center justify-center rounded-full text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-600"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<h3 className="text-center text-lg font-bold">
</h3>
<p className="mt-1 text-center text-sm text-slate-500">
7
</p>
<div className="mt-6 rounded-xl border border-slate-100 bg-slate-50 p-6 text-center">
<div className="text-6xl">🌍</div>
<p className="mt-3 text-sm font-medium text-slate-600">
</p>
<p className="mt-4 text-3xl font-extrabold text-sky-600">¥199</p>
<p className="mt-1 text-xs text-slate-400">
·
</p>
</div>
<div className="mt-5 rounded-xl border border-slate-100 bg-slate-50 p-4 text-center">
<div className="text-4xl">💬</div>
<p className="mt-2 text-sm font-medium text-slate-600">
</p>
<p className="mt-1 text-xs text-slate-400">
· ·
</p>
</div>
</div>
</div>
)}
</div>
);
}