179 lines
7.5 KiB
TypeScript
179 lines
7.5 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { motion } from "framer-motion";
|
||
import { categoryLabels, hotKeywords } from "@/lib/mock-data";
|
||
import { AppResource } from "@/lib/types";
|
||
import { BadgeCheck, ShieldCheck, Download, ArrowRight } from "lucide-react";
|
||
import { buildChannelUrl } from "@/lib/marketing";
|
||
import { useTrack } from "@/hooks/use-track";
|
||
import { TRACKING_EVENTS } from "@/lib/tracking-events";
|
||
|
||
export function SearchBar({ placeholder = "搜索资源..." }: { placeholder?: string }) {
|
||
return (
|
||
<div className="rounded-2xl border border-cyan-300/20 bg-slate-950/30 p-2 backdrop-blur-xl">
|
||
<input
|
||
placeholder={placeholder}
|
||
className="input-fancy border-none bg-transparent py-2"
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function CategoryTabs({
|
||
active,
|
||
onChange,
|
||
}: {
|
||
active: string;
|
||
onChange: (value: string) => void;
|
||
}) {
|
||
return (
|
||
<div className="flex flex-wrap gap-2">
|
||
<button onClick={() => onChange("all")} className={`pill ${active === "all" ? "pill-active" : ""}`}>全部</button>
|
||
{Object.entries(categoryLabels).map(([key, label]) => (
|
||
<button key={key} onClick={() => onChange(key)} className={`pill ${active === key ? "pill-active" : ""}`}>
|
||
{label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function HeroSection() {
|
||
const { track } = useTrack();
|
||
return (
|
||
<motion.section initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} className="panel-strong relative w-full min-w-0 overflow-hidden">
|
||
<div className="pointer-events-none absolute -top-20 right-[-60px] h-56 w-56 rounded-full bg-cyan-300/20 blur-3xl" />
|
||
<div className="pointer-events-none absolute -bottom-24 left-[-80px] h-60 w-60 rounded-full bg-indigo-400/15 blur-3xl" />
|
||
<div className="relative grid gap-6 lg:grid-cols-12 lg:items-center">
|
||
<div className="space-y-4 lg:col-span-8">
|
||
<p className="text-xs uppercase tracking-[0.3em] text-cyan-200/80">Download Nova</p>
|
||
<h1 className="text-3xl font-bold leading-tight tracking-tight md:text-5xl">网盘资源下载,一站直达</h1>
|
||
<p className="max-w-2xl text-sm text-slate-300 md:text-base">每日更新,版本清晰,渠道可查。热门软件、学习资料、影音资源快速下载。</p>
|
||
<SearchBar placeholder="试试搜索:AI 绘图、剪辑、开发工具..." />
|
||
<div className="flex flex-wrap gap-1.5">
|
||
{hotKeywords.map((k) => (
|
||
<span key={k} className="pill">{k}</span>
|
||
))}
|
||
</div>
|
||
<div className="flex flex-wrap gap-3">
|
||
<Link href="/apps" onClick={() => track(TRACKING_EVENTS.ctaClick, { position: "hero", target: "apps" })} className="cta-main">进入资源库</Link>
|
||
<Link href="/unlock" onClick={() => track(TRACKING_EVENTS.ctaClick, { position: "hero", target: "unlock" })} className="cta-sub">输入解锁码</Link>
|
||
</div>
|
||
</div>
|
||
<div className="grid gap-2 sm:grid-cols-3 lg:col-span-4 lg:grid-cols-1">
|
||
<div className="rounded-xl border border-white/10 bg-slate-900/35 p-3">
|
||
<p className="text-xs text-slate-400">今日更新</p>
|
||
<p className="mt-1 text-2xl font-bold text-cyan-200">27</p>
|
||
</div>
|
||
<div className="rounded-xl border border-white/10 bg-slate-900/35 p-3">
|
||
<p className="text-xs text-slate-400">可用资源</p>
|
||
<p className="mt-1 text-2xl font-bold text-cyan-200">1,286</p>
|
||
</div>
|
||
<div className="rounded-xl border border-white/10 bg-slate-900/35 p-3">
|
||
<p className="text-xs text-slate-400">总下载量</p>
|
||
<p className="mt-1 text-2xl font-bold text-cyan-200">8.4M</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</motion.section>
|
||
);
|
||
}
|
||
|
||
export function SafetyBadge({ items }: { items: AppResource["safety"] }) {
|
||
return (
|
||
<div className="flex gap-1 text-emerald-300">
|
||
{items.includes("official") && <BadgeCheck size={14} />}
|
||
{items.includes("verified") && <ShieldCheck size={14} />}
|
||
{items.includes("clean") && <ShieldCheck size={14} />}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function AppCard({ app }: { app: AppResource }) {
|
||
const { track } = useTrack();
|
||
const primaryChannel = app.channels[0];
|
||
const downloadUrl = buildChannelUrl(primaryChannel?.href ?? "#", primaryChannel?.name ?? "direct", app.slug);
|
||
return (
|
||
<motion.article whileHover={{ y: -4 }} className="card-glass flex h-full flex-col gap-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-xl">{app.icon}</span>
|
||
<SafetyBadge items={app.safety} />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-base font-semibold">{app.name}</h3>
|
||
<p className="text-xs text-slate-400">{app.subtitle}</p>
|
||
</div>
|
||
<p className="line-clamp-2 text-xs text-slate-300">{app.description}</p>
|
||
<div className="flex flex-wrap gap-1">
|
||
{app.tags.map((tag) => (
|
||
<span key={tag} className="pill text-[11px]">{tag}</span>
|
||
))}
|
||
</div>
|
||
<div className="mt-auto flex items-center justify-between text-[11px] text-slate-400">
|
||
<span>{app.version}</span>
|
||
<span>{app.size}</span>
|
||
<span>{app.updatedAt}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<Link href={`/apps/${app.slug}`} className="text-sm text-cyan-300">查看详情</Link>
|
||
<a
|
||
href={downloadUrl}
|
||
onClick={() => track(TRACKING_EVENTS.downloadClick, { slug: app.slug, channel: primaryChannel?.name ?? "direct", location: "card" })}
|
||
className="inline-flex items-center gap-1 rounded-xl bg-cyan-400/90 px-3.5 py-2 text-xs font-semibold text-slate-900"
|
||
>
|
||
下载 <Download size={12} />
|
||
</a>
|
||
</div>
|
||
</motion.article>
|
||
);
|
||
}
|
||
|
||
export function AppGrid({ apps }: { apps: AppResource[] }) {
|
||
return <div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">{apps.map((a) => <AppCard key={a.id} app={a} />)}</div>;
|
||
}
|
||
|
||
export function StatsPanel() {
|
||
const stats = [
|
||
{ k: "资源总数", v: "1,286" },
|
||
{ k: "今日更新", v: "27" },
|
||
{ k: "总下载量", v: "8.4M" },
|
||
{ k: "热门分类", v: "AI 工具" },
|
||
];
|
||
return (
|
||
<section className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||
{stats.map((s) => (
|
||
<motion.div key={s.k} initial={{ opacity: 0, y: 10 }} whileInView={{ opacity: 1, y: 0 }} className="card-glass">
|
||
<p className="text-xs text-slate-400">{s.k}</p>
|
||
<p className="mt-2 text-2xl font-bold text-cyan-300">{s.v}</p>
|
||
</motion.div>
|
||
))}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
export function UnlockSection() {
|
||
return (
|
||
<section className="card-glass flex min-w-0 flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
|
||
<div className="min-w-0 flex-1">
|
||
<h3 className="text-lg font-semibold">更多下载方式</h3>
|
||
<p className="mt-1 text-sm text-slate-300">关注公众号可获取解锁码,会员可用更多下载线路。</p>
|
||
</div>
|
||
<div className="flex shrink-0 flex-col gap-2 sm:flex-row sm:flex-wrap sm:justify-end">
|
||
<Link href="/unlock" className="cta-main inline-flex items-center justify-center gap-1 whitespace-nowrap">
|
||
去解锁中心 <ArrowRight size={14} />
|
||
</Link>
|
||
<Link href="/pricing" className="cta-sub inline-flex justify-center whitespace-nowrap">查看会员</Link>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
export function Footer() {
|
||
return (
|
||
<footer className="mt-16 border-t border-white/10 py-8 text-center text-xs text-slate-400">
|
||
<p>Download Nova · 资源下载站</p>
|
||
</footer>
|
||
);
|
||
}
|