Files
gitlab-instance-0a899031_do…/components/sections.tsx
2026-04-02 07:38:11 -05:00

179 lines
7.5 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 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>
);
}