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,20 +15,29 @@ export default async function AppDetailPage({ params }: { params: Promise<{ slug
const appResources = await getApps();
const related = appResources.filter((i) => i.slug !== slug).slice(0, 3);
return (
<div className="space-y-6 py-8">
<div className="space-y-5 py-6 md:space-y-6 md:py-8">
<ScrollReveal>
<section className="card-glass relative overflow-hidden">
<div className="pointer-events-none absolute -right-8 -top-8 h-36 w-36 rounded-full bg-cyan-300/20 blur-3xl" />
<section className="panel-strong">
<div className="grid gap-4 md:grid-cols-12 md:items-center">
<div className="md:col-span-8">
<div className="flex items-center gap-4">
<div className="text-4xl">{app.icon}</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>
<div>
<h1 className="text-3xl font-bold">{app.name}</h1>
<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>
</section>
</ScrollReveal>
<ScrollReveal delay={0.03}>
@@ -46,12 +55,13 @@ export default async function AppDetailPage({ params }: { params: Promise<{ slug
<ScrollReveal delay={0.1}>
<ChangelogTimeline app={app} />
</ScrollReveal>
<section className="grid gap-4 lg:grid-cols-12">
<div className="space-y-4 lg:col-span-8">
<ScrollReveal delay={0.12}>
<section className="card-glass">
<h3 className="text-lg font-semibold"></h3>
<section className="panel">
<h3 className="text-base font-semibold"></h3>
<p className="mt-2 text-sm text-slate-300">{app.description}</p>
<h4 className="mt-4 font-medium">使</h4>
<h4 className="mt-4 text-sm font-medium">使</h4>
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm text-slate-300">
{app.usage.map((u) => <li key={u}>{u}</li>)}
</ul>
@@ -60,15 +70,19 @@ export default async function AppDetailPage({ params }: { params: Promise<{ slug
<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>
<ScrollReveal delay={0.2}>
<section className="space-y-3">
<h3 className="text-lg font-semibold"></h3>
<h3 className="text-base font-semibold"></h3>
<AppGrid apps={related} />
</section>
</ScrollReveal>

View File

@@ -4,9 +4,14 @@ import { getApps } from "@/services/apps.service";
export default async function AppsPage() {
const apps = await getApps();
return (
<div className="space-y-6 py-8">
<h1 className="text-3xl font-bold"></h1>
<p className="text-sm text-slate-300"> mock </p>
<div className="space-y-5 py-6 md:space-y-6 md:py-8">
<section className="panel">
<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} />
</div>
);

View File

@@ -1,11 +1,17 @@
@import "tailwindcss";
:root {
--background: #050816;
--foreground: #e6edf8;
--card: rgba(15, 23, 42, 0.56);
--card-border: rgba(148, 163, 184, 0.24);
--ring: rgba(56, 189, 248, 0.45);
--background: #060b19;
--background-soft: #0b1329;
--foreground: #e8eefc;
--muted: #94a3b8;
--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 {
@@ -17,17 +23,40 @@
@media (prefers-color-scheme: dark) {
:root {
--background: #020617;
--foreground: #e2e8f0;
--background: #050915;
--foreground: #e2e8f8;
}
}
html.light {
--background: #f4f7ff;
--foreground: #111827;
--card: rgba(255, 255, 255, 0.74);
--card-border: rgba(148, 163, 184, 0.24);
--ring: rgba(14, 116, 144, 0.28);
--background: #f3f6ff;
--background-soft: #e9efff;
--foreground: #0f172a;
--muted: #475569;
--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 {
@@ -35,9 +64,12 @@ body {
color: var(--foreground);
font-family: var(--font-geist-sans), Arial, sans-serif;
background-image:
radial-gradient(circle at 10% -10%, rgba(34, 211, 238, 0.18), transparent 35%),
radial-gradient(circle at 90% 0%, rgba(139, 92, 246, 0.2), transparent 36%),
radial-gradient(circle at 50% 100%, rgba(59, 130, 246, 0.1), transparent 45%);
radial-gradient(1000px 440px at 10% -10%, rgba(34, 211, 238, 0.2), transparent 64%),
radial-gradient(900px 460px at 88% -16%, rgba(129, 140, 248, 0.22), transparent 60%),
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 {
@apply rounded-xl border p-3 backdrop-blur-xl transition duration-300;
background: var(--card);
@apply rounded-2xl border p-4 backdrop-blur-xl transition duration-300;
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);
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 {
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 {
@apply rounded-full border px-3.5 py-1.5 text-xs font-medium transition;
background: rgba(148, 163, 184, 0.12);
@apply rounded-full border px-3.5 py-1.5 text-xs font-medium transition duration-200;
background: rgba(148, 163, 184, 0.1);
border-color: rgba(148, 163, 184, 0.24);
}
.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));
border-color: rgba(125, 211, 252, 0.5);
}
.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;
background: linear-gradient(135deg, #22d3ee, #60a5fa 52%, #a78bfa);
box-shadow: 0 10px 28px rgba(56, 189, 248, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.2) inset;
@apply rounded-xl px-4 py-2 text-sm font-semibold text-slate-950 transition duration-200;
background: linear-gradient(120deg, #67e8f9 0%, #a5b4fc 50%, #f9a8d4 100%);
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 {
@apply rounded-xl border px-5 py-2.5 text-sm transition duration-300 hover:-translate-y-0.5;
background: rgba(15, 23, 42, 0.35);
@apply rounded-xl border px-4 py-2 text-sm transition duration-200;
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 {
@apply text-lg font-semibold tracking-tight md:text-xl;
@apply text-xl font-semibold tracking-tight md:text-2xl;
}
.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);
}
.input-fancy:focus {
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 { HeroSection, UnlockSection } from "@/components/sections";
import { AppGrid, HeroSection, StatsPanel, UnlockSection } from "@/components/sections";
import { ScrollReveal } from "@/components/scroll-reveal";
import { getApps } from "@/services/apps.service";
import Link from "next/link";
@@ -7,15 +7,23 @@ import Link from "next/link";
export default async function Home() {
const appResources = await getApps();
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 (
<div className="space-y-8 py-6">
<div className="space-y-8 py-6 md:space-y-10 md:py-8">
<ScrollReveal>
<HeroSection />
</ScrollReveal>
<section className="border-y border-white/10 py-3">
<div className="-mx-1 flex flex-wrap items-center gap-2">
<ScrollReveal delay={0.02}>
<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>
{Object.values(categoryLabels).map((label) => (
<span key={label} className="pill shrink-0">{label}</span>
@@ -23,30 +31,44 @@ export default async function Home() {
</div>
</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">
<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>
<Link href="/apps" className="shrink-0 text-sm text-cyan-300"></Link>
</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) => (
<Link
key={app.id}
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">
<span className="shrink-0 text-lg leading-none">{app.icon}</span>
<div className="col-span-6 flex min-w-0 items-start gap-3 sm:items-center">
<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">
<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>
</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">
<span className="hidden whitespace-nowrap sm:inline">{app.version}</span>
<span className="hidden whitespace-nowrap sm:inline">{app.size}</span>
<span className="whitespace-nowrap tabular-nums">{app.updatedAt}</span>
<div className="col-span-6 grid grid-cols-3 pl-12 text-xs text-slate-300 sm:pl-0 md:text-sm">
<span className="text-center tabular-nums">{app.version}</span>
<span className="text-center tabular-nums">{app.size}</span>
<span className="text-center tabular-nums">{app.updatedAt}</span>
</div>
</Link>
))}
@@ -54,26 +76,29 @@ export default async function Home() {
</div>
<aside className="min-w-0 space-y-4 lg:col-span-4">
<div className="rounded-xl border border-white/10 p-3">
<p className="text-sm font-medium"></p>
<div className="mt-2 space-y-2">
{featured.map((app) => (
<div className="panel">
<div className="mb-2 flex items-center justify-between">
<p className="text-sm font-semibold"></p>
<span className="text-xs text-slate-400"></span>
</div>
<div className="space-y-2">
{latest.slice(0, 6).map((app, index) => (
<Link
key={app.id}
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="shrink-0 tabular-nums">{app.downloads.toLocaleString()}</span>
<span className="flex min-w-0 items-center gap-2.5 truncate">
<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>
))}
</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>
</section>

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">
<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="w-full rounded-xl border border-white/15 bg-slate-900/50 px-3 py-2 text-sm outline-none"
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">
<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,9 +42,14 @@ 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>
<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) => (
@@ -55,6 +60,22 @@ export function HeroSection() {
<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>
);
}