This commit is contained in:
eric
2026-04-02 07:38:11 -05:00
parent d401a36b71
commit d53ede2ea5
8 changed files with 350 additions and 158 deletions

View File

@@ -8,10 +8,10 @@ import { TRACKING_EVENTS } from "@/lib/tracking-events";
export function AppMetaInfo({ app }: { app: AppResource }) {
return (
<div className="grid grid-cols-2 gap-3 text-sm md:grid-cols-4">
<div className="card-glass"><p></p><p className="mt-1 font-semibold">{app.version}</p></div>
<div className="card-glass"><p></p><p className="mt-1 font-semibold">{app.size}</p></div>
<div className="card-glass"><p></p><p className="mt-1 font-semibold">{app.updatedAt}</p></div>
<div className="card-glass"><p></p><p className="mt-1 font-semibold">{app.uploadedAt}</p></div>
<div className="panel-strong"><p className="text-xs text-slate-400"></p><p className="mt-1 font-semibold">{app.version}</p></div>
<div className="panel-strong"><p className="text-xs text-slate-400"></p><p className="mt-1 font-semibold">{app.size}</p></div>
<div className="panel-strong"><p className="text-xs text-slate-400"></p><p className="mt-1 font-semibold">{app.updatedAt}</p></div>
<div className="panel-strong"><p className="text-xs text-slate-400"></p><p className="mt-1 font-semibold">{app.uploadedAt}</p></div>
</div>
);
}
@@ -19,15 +19,15 @@ export function AppMetaInfo({ app }: { app: AppResource }) {
export function DownloadButtons({ app }: { app: AppResource }) {
const { track } = useTrack();
return (
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-4">
{app.channels.map((c) => (
<a
key={c.id}
href={buildChannelUrl(c.href, c.name, app.slug)}
onClick={() => track(TRACKING_EVENTS.downloadClick, { slug: app.slug, channel: c.name, sourceType: c.type, location: "detail" })}
className="rounded-2xl border border-cyan-300/20 bg-gradient-to-br from-cyan-400/15 to-violet-400/10 p-3 text-center text-sm transition hover:-translate-y-1 hover:shadow-[0_10px_30px_rgba(34,211,238,.22)]"
className="panel-strong text-center text-sm transition hover:border-cyan-300/40 hover:bg-cyan-400/10"
>
{c.name}
<p className="font-medium">{c.name}</p>
{c.hint && <p className="mt-1 text-xs text-slate-400">{c.hint}</p>}
</a>
))}
@@ -37,10 +37,10 @@ export function DownloadButtons({ app }: { app: AppResource }) {
export function VersionHistory({ app }: { app: AppResource }) {
return (
<div className="card-glass space-y-3">
<h3 className="text-lg font-semibold"></h3>
<div className="panel space-y-3">
<h3 className="text-base font-semibold"></h3>
{app.versionHistory.map((v) => (
<div key={v.version} className="flex items-center justify-between rounded-2xl border border-white/10 bg-slate-900/20 p-3 text-sm">
<div key={v.version} className="flex items-center justify-between rounded-xl border border-white/10 bg-slate-950/20 p-3 text-sm">
<div><p>{v.version}</p><p className="text-xs text-slate-400">{v.updatedAt} · {v.size}</p></div>
<a href={v.downloadHref} className="pill pill-active"></a>
</div>
@@ -51,8 +51,8 @@ export function VersionHistory({ app }: { app: AppResource }) {
export function ChangelogTimeline({ app }: { app: AppResource }) {
return (
<div className="card-glass space-y-4">
<h3 className="text-lg font-semibold"></h3>
<div className="panel space-y-4">
<h3 className="text-base font-semibold"></h3>
{app.changelog.map((item) => (
<div key={item.date + item.title} className="relative border-l border-cyan-300/40 pl-4">
<span className="absolute -left-[5px] top-1 h-2.5 w-2.5 rounded-full bg-cyan-300" />
@@ -67,8 +67,8 @@ export function ChangelogTimeline({ app }: { app: AppResource }) {
export function QRCodeSection() {
return (
<div className="card-glass text-center">
<div className="mx-auto grid h-40 w-40 place-items-center rounded-2xl border border-dashed border-cyan-300/40 bg-slate-900/70 text-xs text-slate-400">
<div className="panel text-center">
<div className="mx-auto grid h-40 w-40 place-items-center rounded-2xl border border-dashed border-cyan-300/40 bg-slate-900/60 text-xs text-slate-400">
</div>
<p className="mt-3 text-sm text-slate-300"></p>

View File

@@ -1,7 +1,8 @@
"use client";
import { useMemo, useState } from "react";
import { AppGrid, CategoryTabs } from "@/components/sections";
import Link from "next/link";
import { CategoryTabs } from "@/components/sections";
import { AppResource } from "@/lib/types";
export function AppsExplorer({ apps }: { apps: AppResource[] }) {
@@ -24,21 +25,29 @@ export function AppsExplorer({ apps }: { apps: AppResource[] }) {
}, [apps, query, category, sort]);
return (
<section className="space-y-4">
<div className="card-glass space-y-3">
<input
className="w-full rounded-xl border border-white/15 bg-slate-900/50 px-3 py-2 text-sm outline-none"
placeholder="搜索资源、标签或名称"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<section className="space-y-4 md:space-y-5">
<div className="panel space-y-4">
<div className="grid gap-3 md:grid-cols-12 md:items-center">
<div className="md:col-span-8">
<input
className="input-fancy"
placeholder="搜索资源、标签或名称"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<div className="flex items-center justify-between rounded-xl border border-white/10 px-3 py-2 text-xs text-slate-300 md:col-span-4 md:text-sm">
<span></span>
<strong className="text-cyan-200">{filtered.length}</strong>
</div>
</div>
<CategoryTabs active={category} onChange={setCategory} />
<div className="flex items-center gap-2 text-sm">
<span></span>
<div className="flex flex-wrap items-center gap-2 text-sm">
<span className="text-slate-300"></span>
{[
{ id: "latest", label: "最新" },
{ id: "hot", label: "热" },
{ id: "update", label: "更新日期" },
{ id: "latest", label: "最新上架" },
{ id: "hot", label: "热度优先" },
{ id: "update", label: "最近更新" },
].map((s) => (
<button key={s.id} className={`pill ${sort === s.id ? "pill-active" : ""}`} onClick={() => setSort(s.id)}>
{s.label}
@@ -46,11 +55,58 @@ export function AppsExplorer({ apps }: { apps: AppResource[] }) {
))}
</div>
</div>
<AppGrid apps={filtered.slice(0, count)} />
{filtered.length > 0 ? (
<div className="space-y-3">
{filtered.slice(0, count).map((app) => (
<div key={app.id} className="panel-strong flex min-w-0 flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="flex min-w-0 flex-1 items-start gap-3">
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-xl border border-white/10 bg-slate-900/50 text-lg">{app.icon}</span>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Link href={`/apps/${app.slug}`} className="truncate text-sm font-semibold transition hover:text-cyan-300 md:text-base">
{app.name}
</Link>
<span className="rounded-md border border-cyan-300/35 bg-cyan-400/10 px-1.5 py-0.5 text-[10px] text-cyan-100">{app.version}</span>
</div>
<p className="mt-1 truncate text-xs text-slate-400 md:text-sm">{app.subtitle}</p>
<div className="mt-2 flex flex-wrap gap-1.5">
{app.tags.slice(0, 3).map((tag) => (
<span key={tag} className="pill py-1">{tag}</span>
))}
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-2 text-xs text-slate-400 md:w-[320px] md:text-sm">
<div className="rounded-lg border border-white/10 px-2 py-2 text-center">
<p className="text-[10px] uppercase tracking-wide text-slate-500"></p>
<p className="mt-1 text-slate-200">{app.updatedAt}</p>
</div>
<div className="rounded-lg border border-white/10 px-2 py-2 text-center">
<p className="text-[10px] uppercase tracking-wide text-slate-500"></p>
<p className="mt-1 text-slate-200">{app.size}</p>
</div>
<div className="rounded-lg border border-white/10 px-2 py-2 text-center">
<p className="text-[10px] uppercase tracking-wide text-slate-500"></p>
<p className="mt-1 text-slate-200">{app.downloads}</p>
</div>
</div>
</div>
))}
</div>
) : (
<div className="panel text-center">
<p className="text-sm text-slate-300"></p>
<button onClick={() => setQuery("")} className="mt-3 cta-sub"></button>
</div>
)}
{count < filtered.length && (
<button onClick={() => setCount((c) => c + 6)} className="mx-auto block rounded-xl border border-white/20 px-4 py-2 text-sm">
</button>
<div className="pt-1">
<button onClick={() => setCount((c) => c + 6)} className="mx-auto block cta-sub">
</button>
</div>
)}
</section>
);

View File

@@ -1,48 +1,43 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { ThemeToggle } from "@/components/theme-toggle";
import { Search, Sparkles } from "lucide-react";
import { Search } from "lucide-react";
import { HiddenEasterEggTrigger } from "@/components/hidden-easter-egg-trigger";
import { useAuth } from "@/components/auth-provider";
export function Navbar() {
const { session, loginAs, logout } = useAuth();
return (
<motion.header
initial={{ y: -24, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="sticky top-0 z-40 border-b border-white/10 bg-slate-950/70 backdrop-blur-xl"
>
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 md:px-6">
<div className="flex items-center gap-6">
<header className="sticky top-0 z-40 border-b border-white/10 bg-slate-950/70 backdrop-blur-2xl">
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 md:h-18 md:px-6">
<div className="flex min-w-0 items-center gap-4 md:gap-8">
<HiddenEasterEggTrigger>
<Link href="/" className="flex items-center gap-2 text-sm font-semibold">
<Sparkles className="text-cyan-400" size={18} />
Download Nova
<Link href="/" className="flex items-center gap-2.5">
<span className="grid h-8 w-8 place-items-center rounded-xl border border-cyan-300/40 bg-cyan-400/10 text-sm">D</span>
<span className="text-sm font-semibold tracking-wide md:text-base">Download Nova</span>
</Link>
</HiddenEasterEggTrigger>
<nav className="hidden items-center gap-4 text-sm text-slate-300 md:flex">
<Link href="/apps"></Link>
<Link href="/insights"></Link>
<Link href="/unlock"></Link>
<Link href="/pricing"></Link>
<nav className="hidden items-center gap-5 text-sm text-slate-300 lg:flex">
<Link href="/apps" className="transition hover:text-cyan-200"></Link>
<Link href="/insights" className="transition hover:text-cyan-200"></Link>
<Link href="/unlock" className="transition hover:text-cyan-200"></Link>
<Link href="/pricing" className="transition hover:text-cyan-200"></Link>
</nav>
</div>
<div className="flex items-center gap-2">
<div className="flex shrink-0 items-center gap-2.5">
<button className="pill hidden md:inline-flex" onClick={session.isLoggedIn ? logout : () => loginAs()}>
{session.isLoggedIn ? `${session.userName ?? "用户"} · ${session.accessLevel}` : "未登录"}
</button>
<Link href="/apps" className="rounded-full border border-white/20 p-2">
<Link href="/apps" className="rounded-full border border-white/20 p-2 transition hover:border-cyan-300/40 hover:bg-cyan-400/10">
<Search size={16} />
</Link>
<ThemeToggle />
<Link href="/pricing" className="rounded-full bg-cyan-400/90 px-4 py-1.5 text-xs font-semibold text-slate-900 shadow-[0_0_20px_rgba(34,211,238,0.45)]">
<Link href="/pricing" className="cta-main rounded-full px-4 py-1.5 text-xs shadow-none md:text-sm">
/
</Link>
</div>
</div>
</motion.header>
</header>
);
}

View File

@@ -42,18 +42,39 @@ export function CategoryTabs({
export function HeroSection() {
const { track } = useTrack();
return (
<motion.section initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} className="w-full min-w-0 space-y-4 py-4">
<h1 className="text-3xl font-bold leading-tight tracking-tight md:text-4xl"></h1>
<p className="max-w-2xl text-sm text-slate-400"></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>
<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>
);