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

@@ -15,19 +15,28 @@ export default async function AppDetailPage({ params }: { params: Promise<{ slug
const appResources = await getApps(); const appResources = await getApps();
const related = appResources.filter((i) => i.slug !== slug).slice(0, 3); const related = appResources.filter((i) => i.slug !== slug).slice(0, 3);
return ( return (
<div className="space-y-6 py-8"> <div className="space-y-5 py-6 md:space-y-6 md:py-8">
<ScrollReveal> <ScrollReveal>
<section className="card-glass relative overflow-hidden"> <section className="panel-strong">
<div className="pointer-events-none absolute -right-8 -top-8 h-36 w-36 rounded-full bg-cyan-300/20 blur-3xl" /> <div className="grid gap-4 md:grid-cols-12 md:items-center">
<div className="flex items-center gap-4"> <div className="md:col-span-8">
<div className="text-4xl">{app.icon}</div> <div className="flex items-center gap-4">
<div> <div className="grid h-14 w-14 place-items-center rounded-2xl border border-white/10 bg-slate-900/45 text-3xl">{app.icon}</div>
<h1 className="text-3xl font-bold">{app.name}</h1> <div>
<p className="text-slate-300">{app.subtitle}</p> <h1 className="text-2xl font-bold md:text-3xl">{app.name}</h1>
<p className="text-sm text-slate-300 md:text-base">{app.subtitle}</p>
</div>
</div>
<div className="mt-3 flex flex-wrap gap-2">
{app.tags.map((t) => <span key={t} className="pill">{t}</span>)}
</div>
</div>
<div className="md:col-span-4">
<div className="rounded-xl border border-cyan-300/25 bg-cyan-400/10 p-3 text-sm">
<p className="text-xs uppercase tracking-wide text-cyan-200/80"></p>
<p className="mt-2 text-slate-100">使线</p>
</div>
</div> </div>
</div>
<div className="mt-3 flex flex-wrap gap-2">
{app.tags.map((t) => <span key={t} className="pill">{t}</span>)}
</div> </div>
</section> </section>
</ScrollReveal> </ScrollReveal>
@@ -46,31 +55,36 @@ export default async function AppDetailPage({ params }: { params: Promise<{ slug
<ScrollReveal delay={0.1}> <ScrollReveal delay={0.1}>
<ChangelogTimeline app={app} /> <ChangelogTimeline app={app} />
</ScrollReveal> </ScrollReveal>
<section className="grid gap-4 lg:grid-cols-12">
<ScrollReveal delay={0.12}> <div className="space-y-4 lg:col-span-8">
<section className="card-glass"> <ScrollReveal delay={0.12}>
<h3 className="text-lg font-semibold"></h3> <section className="panel">
<p className="mt-2 text-sm text-slate-300">{app.description}</p> <h3 className="text-base font-semibold"></h3>
<h4 className="mt-4 font-medium">使</h4> <p className="mt-2 text-sm text-slate-300">{app.description}</p>
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm text-slate-300"> <h4 className="mt-4 text-sm font-medium">使</h4>
{app.usage.map((u) => <li key={u}>{u}</li>)} <ul className="mt-2 list-disc space-y-1 pl-5 text-sm text-slate-300">
</ul> {app.usage.map((u) => <li key={u}>{u}</li>)}
</ul>
</section>
</ScrollReveal>
<ScrollReveal delay={0.14}>
<ScreenshotGallery slug={app.slug} shots={app.screenshots} />
</ScrollReveal>
</div>
<div className="space-y-4 lg:col-span-4">
<ScrollReveal delay={0.16}>
<QRCodeSection />
</ScrollReveal>
<ScrollReveal delay={0.18}>
<UnlockSection />
</ScrollReveal>
</div>
</section> </section>
</ScrollReveal>
<ScrollReveal delay={0.14}>
<ScreenshotGallery slug={app.slug} shots={app.screenshots} />
</ScrollReveal>
<ScrollReveal delay={0.16}>
<QRCodeSection />
</ScrollReveal>
<ScrollReveal delay={0.18}>
<UnlockSection />
</ScrollReveal>
<ScrollReveal delay={0.2}> <ScrollReveal delay={0.2}>
<section className="space-y-3"> <section className="space-y-3">
<h3 className="text-lg font-semibold"></h3> <h3 className="text-base font-semibold"></h3>
<AppGrid apps={related} /> <AppGrid apps={related} />
</section> </section>
</ScrollReveal> </ScrollReveal>
</div> </div>
); );

View File

@@ -4,9 +4,14 @@ import { getApps } from "@/services/apps.service";
export default async function AppsPage() { export default async function AppsPage() {
const apps = await getApps(); const apps = await getApps();
return ( return (
<div className="space-y-6 py-8"> <div className="space-y-5 py-6 md:space-y-6 md:py-8">
<h1 className="text-3xl font-bold"></h1> <section className="panel">
<p className="text-sm text-slate-300"> mock </p> <p className="text-xs uppercase tracking-[0.25em] text-cyan-200/80">Download Center</p>
<h1 className="mt-2 text-2xl font-bold md:text-3xl"></h1>
<p className="mt-2 max-w-2xl text-sm text-slate-300 md:text-base">
</p>
</section>
<AppsExplorer apps={apps} /> <AppsExplorer apps={apps} />
</div> </div>
); );

View File

@@ -1,11 +1,17 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
--background: #050816; --background: #060b19;
--foreground: #e6edf8; --background-soft: #0b1329;
--card: rgba(15, 23, 42, 0.56); --foreground: #e8eefc;
--card-border: rgba(148, 163, 184, 0.24); --muted: #94a3b8;
--ring: rgba(56, 189, 248, 0.45); --card: rgba(11, 19, 41, 0.72);
--card-strong: rgba(15, 25, 50, 0.9);
--card-border: rgba(148, 163, 184, 0.26);
--ring: rgba(56, 189, 248, 0.38);
--accent: #22d3ee;
--accent-2: #818cf8;
--accent-3: #f472b6;
} }
@theme inline { @theme inline {
@@ -17,17 +23,40 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--background: #020617; --background: #050915;
--foreground: #e2e8f0; --foreground: #e2e8f8;
} }
} }
html.light { html.light {
--background: #f4f7ff; --background: #f3f6ff;
--foreground: #111827; --background-soft: #e9efff;
--card: rgba(255, 255, 255, 0.74); --foreground: #0f172a;
--card-border: rgba(148, 163, 184, 0.24); --muted: #475569;
--ring: rgba(14, 116, 144, 0.28); --card: rgba(255, 255, 255, 0.84);
--card-strong: rgba(255, 255, 255, 0.96);
--card-border: rgba(148, 163, 184, 0.34);
--ring: rgba(14, 116, 144, 0.22);
--accent: #0891b2;
--accent-2: #4f46e5;
--accent-3: #db2777;
}
@keyframes bg-shift {
0% { background-position: 0% 0%, 100% 0%, 50% 0%; }
50% { background-position: 10% 20%, 90% 16%, 50% 100%; }
100% { background-position: 0% 0%, 100% 0%, 50% 0%; }
}
@keyframes panel-glow {
0% { box-shadow: 0 12px 30px rgba(15, 23, 42, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.06); }
50% { box-shadow: 0 14px 34px rgba(34, 211, 238, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.08); }
100% { box-shadow: 0 12px 30px rgba(15, 23, 42, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.06); }
}
@keyframes neon-pulse {
0%, 100% { box-shadow: 0 10px 24px rgba(34, 211, 238, 0.28); }
50% { box-shadow: 0 14px 30px rgba(244, 114, 182, 0.3); }
} }
body { body {
@@ -35,9 +64,12 @@ body {
color: var(--foreground); color: var(--foreground);
font-family: var(--font-geist-sans), Arial, sans-serif; font-family: var(--font-geist-sans), Arial, sans-serif;
background-image: background-image:
radial-gradient(circle at 10% -10%, rgba(34, 211, 238, 0.18), transparent 35%), radial-gradient(1000px 440px at 10% -10%, rgba(34, 211, 238, 0.2), transparent 64%),
radial-gradient(circle at 90% 0%, rgba(139, 92, 246, 0.2), transparent 36%), radial-gradient(900px 460px at 88% -16%, rgba(129, 140, 248, 0.22), transparent 60%),
radial-gradient(circle at 50% 100%, rgba(59, 130, 246, 0.1), transparent 45%); linear-gradient(180deg, var(--background-soft), var(--background));
background-size: 120% 120%, 120% 120%, 100% 100%;
animation: bg-shift 18s ease-in-out infinite;
min-height: 100vh;
} }
* { * {
@@ -45,48 +77,92 @@ body {
} }
.card-glass { .card-glass {
@apply rounded-xl border p-3 backdrop-blur-xl transition duration-300; @apply rounded-2xl border p-4 backdrop-blur-xl transition duration-300;
background: var(--card); background:
linear-gradient(140deg, color-mix(in srgb, var(--accent) 10%, transparent), transparent 40%),
linear-gradient(320deg, color-mix(in srgb, var(--accent-2) 12%, transparent), transparent 50%),
var(--card);
border-color: var(--card-border); border-color: var(--card-border);
box-shadow: 0 20px 55px rgba(2, 8, 23, 0.32), inset 0 1px 0 rgba(255, 255, 255, 0.05); animation: panel-glow 6s ease-in-out infinite;
} }
.card-glass:hover { .card-glass:hover {
box-shadow: 0 24px 62px rgba(2, 8, 23, 0.42), 0 0 0 1px rgba(125, 211, 252, 0.16); box-shadow: 0 18px 42px rgba(34, 211, 238, 0.2);
transform: translateY(-3px) scale(1.01);
} }
.pill { .pill {
@apply rounded-full border px-3.5 py-1.5 text-xs font-medium transition; @apply rounded-full border px-3.5 py-1.5 text-xs font-medium transition duration-200;
background: rgba(148, 163, 184, 0.12); background: rgba(148, 163, 184, 0.1);
border-color: rgba(148, 163, 184, 0.24); border-color: rgba(148, 163, 184, 0.24);
} }
.pill-active { .pill-active {
@apply text-cyan-100; @apply text-cyan-50;
background: linear-gradient(135deg, rgba(6, 182, 212, 0.46), rgba(99, 102, 241, 0.42)); background: linear-gradient(135deg, rgba(6, 182, 212, 0.46), rgba(99, 102, 241, 0.42));
border-color: rgba(125, 211, 252, 0.5); border-color: rgba(125, 211, 252, 0.5);
} }
.cta-main { .cta-main {
@apply rounded-xl px-5 py-2.5 text-sm font-semibold text-slate-900 transition duration-300 hover:-translate-y-0.5; @apply rounded-xl px-4 py-2 text-sm font-semibold text-slate-950 transition duration-200;
background: linear-gradient(135deg, #22d3ee, #60a5fa 52%, #a78bfa); background: linear-gradient(120deg, #67e8f9 0%, #a5b4fc 50%, #f9a8d4 100%);
box-shadow: 0 10px 28px rgba(56, 189, 248, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.2) inset; box-shadow: 0 10px 24px rgba(34, 211, 238, 0.28);
animation: neon-pulse 3.8s ease-in-out infinite;
}
.cta-main:hover {
transform: translateY(-2px) scale(1.015);
box-shadow: 0 14px 32px rgba(99, 102, 241, 0.4);
} }
.cta-sub { .cta-sub {
@apply rounded-xl border px-5 py-2.5 text-sm transition duration-300 hover:-translate-y-0.5; @apply rounded-xl border px-4 py-2 text-sm transition duration-200;
background: rgba(15, 23, 42, 0.35); background: linear-gradient(180deg, rgba(15, 23, 42, 0.32), rgba(15, 23, 42, 0.2));
}
.cta-sub:hover {
border-color: rgba(125, 211, 252, 0.45);
background: rgba(15, 23, 42, 0.4);
} }
.section-title { .section-title {
@apply text-lg font-semibold tracking-tight md:text-xl; @apply text-xl font-semibold tracking-tight md:text-2xl;
} }
.input-fancy { .input-fancy {
@apply w-full rounded-2xl border bg-slate-950/30 px-4 py-3 text-sm outline-none transition placeholder:text-slate-400; @apply w-full rounded-2xl border px-4 py-3 text-sm outline-none transition placeholder:text-slate-400;
background: rgba(15, 23, 42, 0.36);
border-color: rgba(125, 211, 252, 0.2); border-color: rgba(125, 211, 252, 0.2);
} }
.input-fancy:focus { .input-fancy:focus {
box-shadow: 0 0 0 4px var(--ring); box-shadow: 0 0 0 4px var(--ring);
} }
.panel {
@apply rounded-2xl border p-4 md:p-5;
background:
linear-gradient(145deg, color-mix(in srgb, var(--accent) 7%, transparent), transparent 48%),
linear-gradient(315deg, color-mix(in srgb, var(--accent-2) 9%, transparent), transparent 52%),
var(--card);
border-color: var(--card-border);
animation: panel-glow 7s ease-in-out infinite;
}
.panel-strong {
@apply rounded-2xl border p-4 md:p-5;
background:
linear-gradient(140deg, color-mix(in srgb, var(--accent) 12%, transparent), transparent 42%),
linear-gradient(320deg, color-mix(in srgb, var(--accent-3) 12%, transparent), transparent 55%),
var(--card-strong);
border-color: var(--card-border);
animation: panel-glow 5.5s ease-in-out infinite;
}
.text-muted {
color: var(--muted);
}
a, button {
transition: all 0.22s ease;
}

View File

@@ -1,5 +1,5 @@
import { categoryLabels } from "@/lib/mock-data"; import { categoryLabels } from "@/lib/mock-data";
import { HeroSection, UnlockSection } from "@/components/sections"; import { AppGrid, HeroSection, StatsPanel, UnlockSection } from "@/components/sections";
import { ScrollReveal } from "@/components/scroll-reveal"; import { ScrollReveal } from "@/components/scroll-reveal";
import { getApps } from "@/services/apps.service"; import { getApps } from "@/services/apps.service";
import Link from "next/link"; import Link from "next/link";
@@ -7,15 +7,23 @@ import Link from "next/link";
export default async function Home() { export default async function Home() {
const appResources = await getApps(); const appResources = await getApps();
const latest = [...appResources].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); const latest = [...appResources].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
const featured = appResources.filter((a) => a.featured).slice(0, 4); const featured = appResources.filter((a) => a.featured).slice(0, 6);
return ( return (
<div className="space-y-8 py-6"> <div className="space-y-8 py-6 md:space-y-10 md:py-8">
<ScrollReveal> <ScrollReveal>
<HeroSection /> <HeroSection />
</ScrollReveal> </ScrollReveal>
<section className="border-y border-white/10 py-3"> <ScrollReveal delay={0.02}>
<div className="-mx-1 flex flex-wrap items-center gap-2"> <StatsPanel />
</ScrollReveal>
<section className="panel">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-base font-semibold md:text-lg"></h2>
<Link href="/apps" className="text-xs text-cyan-300 md:text-sm"></Link>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="pill pill-active shrink-0"></span> <span className="pill pill-active shrink-0"></span>
{Object.values(categoryLabels).map((label) => ( {Object.values(categoryLabels).map((label) => (
<span key={label} className="pill shrink-0">{label}</span> <span key={label} className="pill shrink-0">{label}</span>
@@ -23,30 +31,44 @@ export default async function Home() {
</div> </div>
</section> </section>
<section className="space-y-3">
<div className="flex min-w-0 flex-wrap items-baseline justify-between gap-2">
<h2 className="section-title shrink-0"></h2>
<Link href="/apps" className="shrink-0 text-sm text-cyan-300"></Link>
</div>
<AppGrid apps={featured} />
</section>
<section className="grid min-w-0 grid-cols-1 gap-6 lg:grid-cols-12"> <section className="grid min-w-0 grid-cols-1 gap-6 lg:grid-cols-12">
<div className="min-w-0 lg:col-span-8"> <div className="min-w-0 lg:col-span-8">
<div className="mb-3 flex min-w-0 flex-wrap items-baseline justify-between gap-2"> <div className="mb-3 flex min-w-0 flex-wrap items-end justify-between gap-2">
<h2 className="section-title shrink-0"></h2> <h2 className="section-title shrink-0"></h2>
<Link href="/apps" className="shrink-0 text-sm text-cyan-300"></Link> <Link href="/apps" className="shrink-0 text-sm text-cyan-300"></Link>
</div> </div>
<div className="divide-y divide-white/10 overflow-hidden rounded-xl border border-white/10"> <div className="panel overflow-hidden p-0">
<div className="hidden grid-cols-12 border-b border-white/10 bg-slate-900/25 px-4 py-2 text-xs text-slate-400 md:grid">
<span className="col-span-6"></span>
<span className="col-span-2 text-center"></span>
<span className="col-span-2 text-center"></span>
<span className="col-span-2 text-center"></span>
</div>
{latest.slice(0, 8).map((app) => ( {latest.slice(0, 8).map((app) => (
<Link <Link
key={app.id} key={app.id}
href={`/apps/${app.slug}`} href={`/apps/${app.slug}`}
className="flex min-w-0 flex-col gap-1 px-3 py-3 transition hover:bg-white/5 sm:flex-row sm:items-center sm:gap-3" className="grid gap-2 border-b border-white/10 px-4 py-3 transition hover:bg-white/5 last:border-b-0 md:grid-cols-12 md:items-center"
> >
<div className="flex min-w-0 flex-1 items-start gap-3 sm:items-center"> <div className="col-span-6 flex min-w-0 items-start gap-3 sm:items-center">
<span className="shrink-0 text-lg leading-none">{app.icon}</span> <span className="grid h-9 w-9 shrink-0 place-items-center rounded-lg border border-white/10 bg-slate-900/45 text-lg leading-none">{app.icon}</span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{app.name}</p> <p className="truncate text-sm font-semibold">{app.name}</p>
<p className="truncate text-xs text-slate-400">{app.subtitle}</p> <p className="truncate text-xs text-slate-400">{app.subtitle}</p>
</div> </div>
</div> </div>
<div className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-1 pl-9 text-xs text-slate-400 sm:pl-0"> <div className="col-span-6 grid grid-cols-3 pl-12 text-xs text-slate-300 sm:pl-0 md:text-sm">
<span className="hidden whitespace-nowrap sm:inline">{app.version}</span> <span className="text-center tabular-nums">{app.version}</span>
<span className="hidden whitespace-nowrap sm:inline">{app.size}</span> <span className="text-center tabular-nums">{app.size}</span>
<span className="whitespace-nowrap tabular-nums">{app.updatedAt}</span> <span className="text-center tabular-nums">{app.updatedAt}</span>
</div> </div>
</Link> </Link>
))} ))}
@@ -54,26 +76,29 @@ export default async function Home() {
</div> </div>
<aside className="min-w-0 space-y-4 lg:col-span-4"> <aside className="min-w-0 space-y-4 lg:col-span-4">
<div className="rounded-xl border border-white/10 p-3"> <div className="panel">
<p className="text-sm font-medium"></p> <div className="mb-2 flex items-center justify-between">
<div className="mt-2 space-y-2"> <p className="text-sm font-semibold"></p>
{featured.map((app) => ( <span className="text-xs text-slate-400"></span>
</div>
<div className="space-y-2">
{latest.slice(0, 6).map((app, index) => (
<Link <Link
key={app.id} key={app.id}
href={`/apps/${app.slug}`} href={`/apps/${app.slug}`}
className="flex min-w-0 items-center justify-between gap-3 text-xs text-slate-300 hover:text-white" className="flex min-w-0 items-center justify-between gap-3 rounded-xl border border-white/10 bg-slate-900/20 px-3 py-2 text-xs text-slate-200 transition hover:border-cyan-300/30 hover:bg-slate-900/35"
> >
<span className="min-w-0 truncate">{app.name}</span> <span className="flex min-w-0 items-center gap-2.5 truncate">
<span className="shrink-0 tabular-nums">{app.downloads.toLocaleString()}</span> <span className="grid h-6 w-6 shrink-0 place-items-center rounded-full border border-cyan-300/35 bg-cyan-400/10 text-[10px] font-semibold text-cyan-200">
{String(index + 1).padStart(2, "0")}
</span>
<span className="truncate">{app.name}</span>
</span>
<span className="shrink-0 tabular-nums text-cyan-200">{app.downloads.toLocaleString()}</span>
</Link> </Link>
))} ))}
</div> </div>
</div> </div>
<div className="rounded-xl border border-white/10 p-3 text-xs leading-relaxed text-slate-300">
<p> 70%</p>
<p className="mt-1"> 20%</p>
<p className="mt-1"> 10%</p>
</div>
</aside> </aside>
</section> </section>

View File

@@ -8,10 +8,10 @@ import { TRACKING_EVENTS } from "@/lib/tracking-events";
export function AppMetaInfo({ app }: { app: AppResource }) { export function AppMetaInfo({ app }: { app: AppResource }) {
return ( return (
<div className="grid grid-cols-2 gap-3 text-sm md:grid-cols-4"> <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="panel-strong"><p className="text-xs text-slate-400"></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="panel-strong"><p className="text-xs text-slate-400"></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="panel-strong"><p className="text-xs text-slate-400"></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.uploadedAt}</p></div>
</div> </div>
); );
} }
@@ -19,15 +19,15 @@ export function AppMetaInfo({ app }: { app: AppResource }) {
export function DownloadButtons({ app }: { app: AppResource }) { export function DownloadButtons({ app }: { app: AppResource }) {
const { track } = useTrack(); const { track } = useTrack();
return ( 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) => ( {app.channels.map((c) => (
<a <a
key={c.id} key={c.id}
href={buildChannelUrl(c.href, c.name, app.slug)} href={buildChannelUrl(c.href, c.name, app.slug)}
onClick={() => track(TRACKING_EVENTS.downloadClick, { slug: app.slug, channel: c.name, sourceType: c.type, location: "detail" })} 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>} {c.hint && <p className="mt-1 text-xs text-slate-400">{c.hint}</p>}
</a> </a>
))} ))}
@@ -37,10 +37,10 @@ export function DownloadButtons({ app }: { app: AppResource }) {
export function VersionHistory({ app }: { app: AppResource }) { export function VersionHistory({ app }: { app: AppResource }) {
return ( return (
<div className="card-glass space-y-3"> <div className="panel space-y-3">
<h3 className="text-lg font-semibold"></h3> <h3 className="text-base font-semibold"></h3>
{app.versionHistory.map((v) => ( {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> <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> <a href={v.downloadHref} className="pill pill-active"></a>
</div> </div>
@@ -51,8 +51,8 @@ export function VersionHistory({ app }: { app: AppResource }) {
export function ChangelogTimeline({ app }: { app: AppResource }) { export function ChangelogTimeline({ app }: { app: AppResource }) {
return ( return (
<div className="card-glass space-y-4"> <div className="panel space-y-4">
<h3 className="text-lg font-semibold"></h3> <h3 className="text-base font-semibold"></h3>
{app.changelog.map((item) => ( {app.changelog.map((item) => (
<div key={item.date + item.title} className="relative border-l border-cyan-300/40 pl-4"> <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" /> <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() { export function QRCodeSection() {
return ( return (
<div className="card-glass text-center"> <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/70 text-xs text-slate-400"> <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> </div>
<p className="mt-3 text-sm text-slate-300"></p> <p className="mt-3 text-sm text-slate-300"></p>

View File

@@ -1,7 +1,8 @@
"use client"; "use client";
import { useMemo, useState } from "react"; 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"; import { AppResource } from "@/lib/types";
export function AppsExplorer({ apps }: { apps: AppResource[] }) { export function AppsExplorer({ apps }: { apps: AppResource[] }) {
@@ -24,21 +25,29 @@ export function AppsExplorer({ apps }: { apps: AppResource[] }) {
}, [apps, query, category, sort]); }, [apps, query, category, sort]);
return ( return (
<section className="space-y-4"> <section className="space-y-4 md:space-y-5">
<div className="card-glass space-y-3"> <div className="panel space-y-4">
<input <div className="grid gap-3 md:grid-cols-12 md:items-center">
className="w-full rounded-xl border border-white/15 bg-slate-900/50 px-3 py-2 text-sm outline-none" <div className="md:col-span-8">
placeholder="搜索资源、标签或名称" <input
value={query} className="input-fancy"
onChange={(e) => setQuery(e.target.value)} 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} /> <CategoryTabs active={category} onChange={setCategory} />
<div className="flex items-center gap-2 text-sm"> <div className="flex flex-wrap items-center gap-2 text-sm">
<span></span> <span className="text-slate-300"></span>
{[ {[
{ id: "latest", label: "最新" }, { id: "latest", label: "最新上架" },
{ id: "hot", label: "热" }, { id: "hot", label: "热度优先" },
{ id: "update", label: "更新日期" }, { id: "update", label: "最近更新" },
].map((s) => ( ].map((s) => (
<button key={s.id} className={`pill ${sort === s.id ? "pill-active" : ""}`} onClick={() => setSort(s.id)}> <button key={s.id} className={`pill ${sort === s.id ? "pill-active" : ""}`} onClick={() => setSort(s.id)}>
{s.label} {s.label}
@@ -46,11 +55,58 @@ export function AppsExplorer({ apps }: { apps: AppResource[] }) {
))} ))}
</div> </div>
</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 && ( {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"> <div className="pt-1">
<button onClick={() => setCount((c) => c + 6)} className="mx-auto block cta-sub">
</button>
</button>
</div>
)} )}
</section> </section>
); );

View File

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

View File

@@ -42,18 +42,39 @@ export function CategoryTabs({
export function HeroSection() { export function HeroSection() {
const { track } = useTrack(); const { track } = useTrack();
return ( return (
<motion.section initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} className="w-full min-w-0 space-y-4 py-4"> <motion.section initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} className="panel-strong relative w-full min-w-0 overflow-hidden">
<h1 className="text-3xl font-bold leading-tight tracking-tight md:text-4xl"></h1> <div className="pointer-events-none absolute -top-20 right-[-60px] h-56 w-56 rounded-full bg-cyan-300/20 blur-3xl" />
<p className="max-w-2xl text-sm text-slate-400"></p> <div className="pointer-events-none absolute -bottom-24 left-[-80px] h-60 w-60 rounded-full bg-indigo-400/15 blur-3xl" />
<SearchBar placeholder="试试搜索AI 绘图、剪辑、开发工具..." /> <div className="relative grid gap-6 lg:grid-cols-12 lg:items-center">
<div className="flex flex-wrap gap-1.5"> <div className="space-y-4 lg:col-span-8">
{hotKeywords.map((k) => ( <p className="text-xs uppercase tracking-[0.3em] text-cyan-200/80">Download Nova</p>
<span key={k} className="pill">{k}</span> <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>
</div> <SearchBar placeholder="试试搜索AI 绘图、剪辑、开发工具..." />
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-1.5">
<Link href="/apps" onClick={() => track(TRACKING_EVENTS.ctaClick, { position: "hero", target: "apps" })} className="cta-main"></Link> {hotKeywords.map((k) => (
<Link href="/unlock" onClick={() => track(TRACKING_EVENTS.ctaClick, { position: "hero", target: "unlock" })} className="cta-sub"></Link> <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> </div>
</motion.section> </motion.section>
); );