This commit is contained in:
eric
2026-03-14 00:28:34 -05:00
parent cf1e3760c2
commit 1f8c9c6d26
88 changed files with 5463 additions and 22657 deletions

View File

@@ -0,0 +1,75 @@
"use client";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { motion } from "framer-motion";
import { CONTENT_PREVIEW_ITEMS } from "~/lib/data";
const TAG_COLORS: Record<string, string> = {
: "bg-blue-50 text-blue-700",
: "bg-emerald-50 text-emerald-700",
: "bg-amber-50 text-amber-700",
: "bg-rose-50 text-rose-700",
: "bg-violet-50 text-violet-700",
: "bg-neutral-100 text-neutral-700",
};
export function ContentPreview() {
return (
<section className="bg-white py-16 md:py-24">
<div className="container mx-auto px-4 md:px-6">
<motion.h2
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
className="text-2xl font-semibold text-neutral-900 md:text-3xl"
>
</motion.h2>
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="mt-2 text-neutral-600"
>
</motion.p>
<div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{CONTENT_PREVIEW_ITEMS.map((item, i) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ delay: Math.min(i * 0.05, 0.2) }}
>
<Link
href={item.href}
target="_blank"
rel="noopener noreferrer"
className="group block rounded-xl border border-neutral-200 p-5 transition-all hover:border-neutral-300 hover:shadow-md"
>
<span
className={`inline-block rounded-md px-2 py-0.5 text-xs font-medium ${
TAG_COLORS[item.type] ?? "bg-neutral-100 text-neutral-600"
}`}
>
{item.tag}
</span>
<h3 className="mt-3 font-medium text-neutral-900">
{item.title}
</h3>
<p className="mt-1 text-sm text-neutral-500">{item.summary}</p>
<span className="mt-3 inline-flex items-center gap-1 text-sm font-medium text-neutral-600 group-hover:text-neutral-900">
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</span>
</Link>
</motion.div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,63 @@
"use client";
import { MapPin, BookOpen, Crown } from "lucide-react";
import { motion } from "framer-motion";
import { ECOSYSTEM_COLUMNS } from "~/lib/data";
const ICON_MAP = [MapPin, BookOpen, Crown] as const;
export function Ecosystem() {
return (
<section className="bg-neutral-50 py-16 md:py-24">
<div className="container mx-auto px-4 md:px-6">
<motion.h2
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
className="text-center text-2xl font-semibold text-neutral-900 md:text-3xl"
>
</motion.h2>
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="mx-auto mt-3 max-w-2xl text-center text-neutral-600"
>
</motion.p>
<div className="mt-12 grid gap-6 md:grid-cols-3">
{ECOSYSTEM_COLUMNS.map((col, i) => {
const Icon = ICON_MAP[i];
return (
<motion.div
key={col.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ delay: i * 0.08 }}
className="rounded-xl border border-neutral-200 bg-white p-6"
>
<Icon className="h-9 w-9 text-neutral-600" />
<h3 className="mt-4 font-medium text-neutral-900">
{col.title}
</h3>
<div className="mt-3 flex flex-wrap gap-2">
{col.items.map((item) => (
<span
key={item}
className="rounded-md bg-neutral-100 px-2.5 py-1 text-xs text-neutral-600"
>
{item}
</span>
))}
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,78 @@
"use client";
import Link from "next/link";
import { MapPin, BookOpen, Crown, ArrowRight } from "lucide-react";
import { motion } from "framer-motion";
import { ENTRY_PATHS } from "~/lib/data";
const ICON_MAP = {
MapPin,
BookOpen,
Crown,
} as const;
const ACCENT_STYLES = {
blue: "border-l-blue-500/80 hover:border-l-blue-500",
green: "border-l-emerald-500/80 hover:border-l-emerald-500",
amber: "border-l-amber-500/80 hover:border-l-amber-500",
} as const;
export function EntryGrid() {
return (
<section className="bg-white py-16 md:py-24">
<div className="container mx-auto px-4 md:px-6">
<motion.h2
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
className="text-center text-2xl font-semibold text-neutral-900 md:text-3xl"
>
</motion.h2>
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="mx-auto mt-3 max-w-2xl text-center text-neutral-600"
>
</motion.p>
<div className="mt-12 grid gap-6 md:grid-cols-3">
{ENTRY_PATHS.map((path, i) => {
const Icon = ICON_MAP[path.icon];
const accent = ACCENT_STYLES[path.accent];
return (
<motion.div
key={path.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ delay: i * 0.08 }}
>
<Link
href={path.href}
target="_blank"
rel="noopener noreferrer"
className={`group block rounded-xl border border-neutral-200 bg-white p-6 transition-all hover:-translate-y-1 hover:shadow-lg ${accent} border-l-4`}
>
<Icon className="h-10 w-10 text-neutral-600" />
<h3 className="mt-4 text-lg font-semibold text-neutral-900">
{path.title}
</h3>
<p className="mt-2 text-sm text-neutral-600">
{path.description}
</p>
<span className="mt-4 inline-flex items-center gap-1 text-sm font-medium text-neutral-700 group-hover:text-neutral-900">
{path.cta}
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</span>
</Link>
</motion.div>
);
})}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,63 @@
"use client";
import Link from "next/link";
import { MapPin, BookOpen, Crown } from "lucide-react";
import { motion } from "framer-motion";
import { FINAL_CTAS } from "~/lib/data";
import { Button } from "~/components/ui/button";
const ICONS = [MapPin, BookOpen, Crown] as const;
export function FinalCta() {
return (
<section className="bg-neutral-900 py-16 text-white md:py-24">
<div className="container mx-auto px-4 md:px-6">
<motion.h2
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
className="text-center text-2xl font-semibold md:text-3xl"
>
</motion.h2>
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="mx-auto mt-3 max-w-xl text-center text-neutral-300"
>
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="mt-12 flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:justify-center"
>
{FINAL_CTAS.map((cta, i) => {
const Icon = ICONS[i];
return (
<Link
key={cta.label}
href={cta.href}
target="_blank"
rel="noopener noreferrer"
className="group"
>
<Button
variant="outline"
size="lg"
className="w-full min-w-[200px] border-neutral-600 bg-transparent text-white hover:bg-neutral-800 hover:text-white sm:w-auto"
>
<Icon className="mr-2 h-4 w-4" />
{cta.label}
</Button>
</Link>
);
})}
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,141 @@
"use client";
import Link from "next/link";
import { MapPin, BookOpen, Crown } from "lucide-react";
import { motion } from "framer-motion";
import { HERO_TAGS } from "~/lib/data";
import { Button } from "~/components/ui/button";
const HERO_CARDS = [
{
title: "探索城市",
desc: "找到适合远程生活的地方",
icon: MapPin,
href: "https://meetup.hackrobot.cn",
},
{
title: "学习专题",
desc: "从零搭建你的技能与收入结构",
icon: BookOpen,
href: "https://digital.hackrobot.cn",
},
{
title: "会员社区",
desc: "进入更高密度的人脉与资源网络",
icon: Crown,
href: "https://vip.hackrobot.cn",
},
];
export function Hero() {
return (
<section className="relative overflow-hidden bg-white">
<div className="container mx-auto px-4 py-16 md:px-6 md:py-24 lg:py-32">
<div className="grid gap-12 lg:grid-cols-2 lg:gap-16">
<div className="flex flex-col justify-center">
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<h1 className="text-3xl font-semibold tracking-tight text-neutral-900 sm:text-4xl md:text-5xl">
</h1>
<p className="mt-4 text-lg text-neutral-600 md:text-xl">
</p>
<p className="mt-2 text-base text-neutral-500">
</p>
</motion.div>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.4 }}
className="mt-6 max-w-lg text-sm text-neutral-500"
>
+ + +
</motion.p>
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.4 }}
className="mt-8 flex flex-wrap gap-3"
>
<Link
href="https://meetup.hackrobot.cn"
target="_blank"
rel="noopener noreferrer"
>
<Button size="lg" className="min-w-[180px]">
</Button>
</Link>
<Link
href="https://meetup.hackrobot.cn"
target="_blank"
rel="noopener noreferrer"
>
<Button variant="outline" size="lg">
</Button>
</Link>
<Link
href="https://digital.hackrobot.cn"
target="_blank"
rel="noopener noreferrer"
>
<Button variant="ghost" size="lg">
</Button>
</Link>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="mt-10 flex flex-wrap gap-2"
>
{HERO_TAGS.map((tag, i) => (
<span
key={tag}
className="rounded-full border border-neutral-200 bg-neutral-50 px-3 py-1 text-xs text-neutral-600"
>
{tag}
</span>
))}
</motion.div>
</div>
<motion.div
initial={{ opacity: 0, x: 24 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4, duration: 0.5 }}
className="flex flex-col gap-4 lg:justify-center"
>
<div className="grid gap-4 sm:grid-cols-3">
{HERO_CARDS.map((card, i) => (
<Link
key={card.title}
href={card.href}
target="_blank"
rel="noopener noreferrer"
className="group flex flex-col rounded-xl border border-neutral-200 bg-white p-5 shadow-sm transition-all hover:-translate-y-1 hover:shadow-md"
>
<card.icon className="h-8 w-8 text-neutral-400 transition-colors group-hover:text-neutral-700" />
<h3 className="mt-3 font-medium text-neutral-900">
{card.title}
</h3>
<p className="mt-1 text-sm text-neutral-500">{card.desc}</p>
</Link>
))}
</div>
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,70 @@
"use client";
import { MapPin, Wallet, Wrench, Users } from "lucide-react";
import { motion } from "framer-motion";
import { METHODOLOGY_SYSTEMS } from "~/lib/data";
const ICON_MAP = {
location: MapPin,
income: Wallet,
tools: Wrench,
community: Users,
} as const;
export function Methodology() {
return (
<section className="bg-neutral-50 py-16 md:py-24">
<div className="container mx-auto px-4 md:px-6">
<motion.h2
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
className="text-center text-2xl font-semibold text-neutral-900 md:text-3xl"
>
</motion.h2>
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="mx-auto mt-3 max-w-2xl text-center text-neutral-600"
>
</motion.p>
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{METHODOLOGY_SYSTEMS.map((sys, i) => {
const Icon = ICON_MAP[sys.id];
return (
<motion.div
key={sys.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ delay: i * 0.05 }}
className="rounded-xl border border-neutral-200 bg-white p-6"
>
<Icon className="h-9 w-9 text-neutral-600" />
<h3 className="mt-4 font-medium text-neutral-900">
{sys.title}
</h3>
<p className="mt-2 text-sm text-neutral-600">
{sys.description}
</p>
</motion.div>
);
})}
</div>
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="mx-auto mt-12 max-w-2xl text-center text-sm text-neutral-600"
>
</motion.p>
</div>
</section>
);
}

View File

@@ -0,0 +1,57 @@
"use client";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { motion } from "framer-motion";
import { SCENARIOS } from "~/lib/data";
export function Scenarios() {
return (
<section className="bg-white py-16 md:py-24">
<div className="container mx-auto px-4 md:px-6">
<motion.h2
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
className="text-center text-2xl font-semibold text-neutral-900 md:text-3xl"
>
</motion.h2>
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="mx-auto mt-3 max-w-2xl text-center text-neutral-600"
>
</motion.p>
<div className="mt-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{SCENARIOS.map((s, i) => (
<motion.div
key={s.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ delay: i * 0.06 }}
>
<Link
href={s.href}
target="_blank"
rel="noopener noreferrer"
className="group block rounded-xl border border-neutral-200 bg-neutral-50/50 p-5 transition-all hover:border-neutral-300 hover:bg-white hover:shadow-md"
>
<p className="font-medium text-neutral-900">{s.pain}</p>
<p className="mt-1 text-sm text-neutral-500">{s.path}</p>
<span className="mt-3 inline-flex items-center gap-1 text-sm font-medium text-neutral-700 group-hover:text-neutral-900">
{s.cta}
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</span>
</Link>
</motion.div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,56 @@
"use client";
import { Target, Layers, Handshake } from "lucide-react";
import { motion } from "framer-motion";
import { TRUST_PRINCIPLES } from "~/lib/data";
const ICON_MAP = [Target, Layers, Handshake] as const;
export function Trust() {
return (
<section className="bg-neutral-50 py-16 md:py-24">
<div className="container mx-auto px-4 md:px-6">
<motion.h2
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
className="text-center text-2xl font-semibold text-neutral-900 md:text-3xl"
>
</motion.h2>
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="mx-auto mt-3 max-w-2xl text-center text-neutral-600"
>
</motion.p>
<div className="mt-12 grid gap-6 md:grid-cols-3">
{TRUST_PRINCIPLES.map((p, i) => {
const Icon = ICON_MAP[i];
return (
<motion.div
key={p.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ delay: i * 0.06 }}
className="rounded-xl border border-neutral-200 bg-white p-6 text-center"
>
<Icon className="mx-auto h-9 w-9 text-neutral-600" />
<h3 className="mt-4 font-medium text-neutral-900">
{p.title}
</h3>
<p className="mt-2 text-sm text-neutral-600">
{p.description}
</p>
</motion.div>
);
})}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,61 @@
"use client";
import { MapPin, TrendingUp, Zap, Users } from "lucide-react";
import { motion } from "framer-motion";
import { VALUE_PROPOSITIONS } from "~/lib/data";
const ICON_MAP = {
MapPin,
TrendingUp,
Zap,
Users,
} as const;
export function ValueGrid() {
return (
<section className="bg-neutral-50 py-16 md:py-24">
<div className="container mx-auto px-4 md:px-6">
<motion.h2
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
className="text-center text-2xl font-semibold text-neutral-900 md:text-3xl"
>
</motion.h2>
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="mx-auto mt-3 max-w-2xl text-center text-neutral-600"
>
</motion.p>
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{VALUE_PROPOSITIONS.map((item, i) => {
const Icon = ICON_MAP[item.icon];
return (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ delay: i * 0.05 }}
className="rounded-xl border border-neutral-200 bg-white p-6 shadow-sm transition-all hover:shadow-md"
>
<Icon className="h-10 w-10 text-neutral-600" />
<h3 className="mt-4 font-medium text-neutral-900">
{item.title}
</h3>
<p className="mt-2 text-sm text-neutral-600">
{item.description}
</p>
</motion.div>
);
})}
</div>
</div>
</section>
);
}