This commit is contained in:
eric
2026-04-02 07:15:42 -05:00
parent 644a6ac3a9
commit d401a36b71
62 changed files with 13264 additions and 78 deletions

77
app/apps/[slug]/page.tsx Normal file
View File

@@ -0,0 +1,77 @@
import { notFound } from "next/navigation";
import { AppMetaInfo, ChangelogTimeline, DownloadButtons, QRCodeSection, VersionHistory } from "@/components/app-detail";
import { AppGrid, UnlockSection } from "@/components/sections";
import { ScrollReveal } from "@/components/scroll-reveal";
import { DetailScrollShowcase } from "@/components/detail-scroll-showcase";
import { getAppBySlug, getApps } from "@/services/apps.service";
import { ScreenshotGallery } from "@/components/screenshot-gallery";
import { AccessGate } from "@/components/access-gate";
export default async function AppDetailPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const app = await getAppBySlug(slug);
if (!app) return notFound();
const appResources = await getApps();
const related = appResources.filter((i) => i.slug !== slug).slice(0, 3);
return (
<div className="space-y-6 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" />
<div className="flex items-center gap-4">
<div className="text-4xl">{app.icon}</div>
<div>
<h1 className="text-3xl font-bold">{app.name}</h1>
<p className="text-slate-300">{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>
</section>
</ScrollReveal>
<ScrollReveal delay={0.03}>
<AppMetaInfo app={app} />
</ScrollReveal>
<ScrollReveal delay={0.06}>
<AccessGate slug={app.slug}>
<DownloadButtons app={app} />
</AccessGate>
</ScrollReveal>
<DetailScrollShowcase name={app.name} />
<ScrollReveal delay={0.08}>
<VersionHistory app={app} />
</ScrollReveal>
<ScrollReveal delay={0.1}>
<ChangelogTimeline app={app} />
</ScrollReveal>
<ScrollReveal delay={0.12}>
<section className="card-glass">
<h3 className="text-lg font-semibold"></h3>
<p className="mt-2 text-sm text-slate-300">{app.description}</p>
<h4 className="mt-4 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>
</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}>
<section className="space-y-3">
<h3 className="text-lg font-semibold"></h3>
<AppGrid apps={related} />
</section>
</ScrollReveal>
</div>
);
}

13
app/apps/page.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { AppsExplorer } from "@/components/apps-explorer";
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>
<AppsExplorer apps={apps} />
</div>
);
}

43
app/checkout/page.tsx Normal file
View File

@@ -0,0 +1,43 @@
import Link from "next/link";
import { createCheckoutOrder } from "@/services/order.service";
export default async function CheckoutPage({
searchParams,
}: {
searchParams: Promise<{ plan?: string; slug?: string }>;
}) {
const { plan: rawPlan = "pro", slug = "unknown-app" } = await searchParams;
const plan = rawPlan === "free" || rawPlan === "pro" || rawPlan === "ultra" ? rawPlan : "pro";
const order = await createCheckoutOrder(plan, slug);
return (
<div className="space-y-6 py-8">
<section className="card-glass">
<p className="text-xs text-cyan-300"></p>
<h1 className="mt-1 text-3xl font-bold"></h1>
<p className="mt-2 text-sm text-slate-300">{order.plan.toUpperCase()} · {order.slug}</p>
</section>
<section className="grid gap-4 md:grid-cols-2">
<div className="card-glass space-y-2 text-sm">
<p>{order.id}</p>
<p>¥{order.amount}</p>
<p>PayJS</p>
<p className="text-slate-400"></p>
</div>
<div className="card-glass grid place-items-center">
<div className="grid h-44 w-44 place-items-center rounded-2xl border border-dashed border-cyan-300/40 text-xs text-slate-400">
</div>
</div>
</section>
<section className="card-glass flex flex-wrap gap-2">
<Link href={`/orders/${order.id}?status=pending&plan=${order.plan}&slug=${order.slug}`} className="cta-main">
</Link>
<Link href="/pricing" className="cta-sub"></Link>
</section>
</div>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

92
app/globals.css Normal file
View File

@@ -0,0 +1,92 @@
@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);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #020617;
--foreground: #e2e8f0;
}
}
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);
}
body {
background: var(--background);
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%);
}
* {
border-color: var(--card-border);
}
.card-glass {
@apply rounded-xl border p-3 backdrop-blur-xl transition duration-300;
background: 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);
}
.card-glass:hover {
box-shadow: 0 24px 62px rgba(2, 8, 23, 0.42), 0 0 0 1px rgba(125, 211, 252, 0.16);
}
.pill {
@apply rounded-full border px-3.5 py-1.5 text-xs font-medium transition;
background: rgba(148, 163, 184, 0.12);
border-color: rgba(148, 163, 184, 0.24);
}
.pill-active {
@apply text-cyan-100;
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;
}
.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);
}
.section-title {
@apply text-lg font-semibold tracking-tight md:text-xl;
}
.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;
border-color: rgba(125, 211, 252, 0.2);
}
.input-fancy:focus {
box-shadow: 0 0 0 4px var(--ring);
}

44
app/insights/page.tsx Normal file
View File

@@ -0,0 +1,44 @@
import { InsightChart } from "@/components/insight-chart";
import { InsightBarChart, InsightPieChart } from "@/components/insight-extra-charts";
const rank = [
["Media Spark", 29430],
["Nova AI Studio", 28810],
["Pixel Flow", 19840],
["DevLight Proxy", 11690],
];
export default function InsightsPage() {
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="card-glass">
<h3 className="mb-2 text-lg font-semibold"></h3>
<InsightChart />
</div>
<section className="grid gap-4 md:grid-cols-2">
<div className="card-glass">
<h3 className="mb-2 font-semibold"></h3>
<InsightBarChart />
</div>
<div className="card-glass">
<h3 className="mb-2 font-semibold"></h3>
<InsightPieChart />
</div>
</section>
<section className="grid gap-4 md:grid-cols-2">
<div className="card-glass">
<h3 className="mb-2 font-semibold"></h3>
{rank.map(([n, v]) => (
<div key={n} className="mb-2 flex items-center justify-between text-sm"><span>{n}</span><span>{v}</span></div>
))}
</div>
<div className="card-glass">
<h3 className="mb-2 mt-4 font-semibold"></h3>
<p className="text-sm text-slate-300"> 39% · 33% · 21% · 线 7%</p>
</div>
</section>
</div>
);
}

55
app/layout.tsx Normal file
View File

@@ -0,0 +1,55 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { Navbar } from "@/components/navbar";
import { Footer } from "@/components/sections";
import { AmbientBackground } from "@/components/ambient-background";
import { OpsPopup } from "@/components/ops-popup";
import { EventDebugPanel } from "@/components/event-debug-panel";
import { AuthProvider } from "@/components/auth-provider";
import { DeveloperFlagsProvider } from "@/components/developer-flags-provider";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Download Nova",
description: "高颜值下载站前端Next.js",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="zh-CN"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
suppressHydrationWarning
>
<body className="min-h-full flex flex-col bg-slate-950 text-slate-100">
<ThemeProvider>
<DeveloperFlagsProvider>
<AuthProvider>
<AmbientBackground />
<Navbar />
<main className="mx-auto w-full max-w-7xl flex-1 px-4 md:px-6">{children}</main>
<Footer />
<OpsPopup />
<EventDebugPanel />
</AuthProvider>
</DeveloperFlagsProvider>
</ThemeProvider>
</body>
</html>
);
}

49
app/orders/[id]/page.tsx Normal file
View File

@@ -0,0 +1,49 @@
import Link from "next/link";
import { getOrderById } from "@/services/order.service";
import { OrderActivation } from "@/components/order-activation";
const statusMap = {
pending: { label: "待支付确认", color: "text-amber-200", tips: "系统正在等待支付回调,请稍后刷新。" },
success: { label: "支付成功", color: "text-emerald-200", tips: "会员权益已激活,可返回资源详情页下载。" },
failed: { label: "支付失败", color: "text-rose-200", tips: "支付未完成,请重试或更换支付方式。" },
} as const;
export default async function OrderStatusPage({
params,
searchParams,
}: {
params: Promise<{ id: string }>;
searchParams: Promise<{ status?: "pending" | "success" | "failed"; plan?: string; slug?: string }>;
}) {
const { id } = await params;
const { status: rawStatus = "pending", plan: rawPlan = "pro", slug = "unknown-app" } = await searchParams;
const status = rawStatus === "pending" || rawStatus === "success" || rawStatus === "failed" ? rawStatus : "pending";
const plan = rawPlan === "free" || rawPlan === "pro" || rawPlan === "ultra" ? rawPlan : "pro";
const order = await getOrderById({ id, plan, slug, status });
const info = statusMap[order.status] ?? statusMap.pending;
return (
<div className="space-y-6 py-8">
<section className="card-glass">
<p className="text-xs text-cyan-300"></p>
<h1 className="mt-1 text-3xl font-bold"> {order.id}</h1>
<p className={`mt-2 text-lg font-semibold ${info.color}`}>{info.label}</p>
<p className="mt-2 text-sm text-slate-300">{info.tips}</p>
</section>
<section className="card-glass text-sm text-slate-300">
<p>{order.plan.toUpperCase()}</p>
<p>{order.slug}</p>
<p>{order.status}</p>
<p>¥{order.amount}</p>
</section>
<section className="card-glass flex flex-wrap gap-2">
<OrderActivation slug={order.slug} canActivate={order.status === "success"} />
<Link href="/pricing" className="cta-sub"></Link>
<Link href={`/orders/${order.id}?status=success&plan=${order.plan}&slug=${order.slug}`} className="cta-sub"></Link>
<Link href={`/orders/${order.id}?status=failed&plan=${order.plan}&slug=${order.slug}`} className="cta-sub"></Link>
</section>
</div>
);
}

85
app/page.tsx Normal file
View File

@@ -0,0 +1,85 @@
import { categoryLabels } from "@/lib/mock-data";
import { HeroSection, UnlockSection } from "@/components/sections";
import { ScrollReveal } from "@/components/scroll-reveal";
import { getApps } from "@/services/apps.service";
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);
return (
<div className="space-y-8 py-6">
<ScrollReveal>
<HeroSection />
</ScrollReveal>
<section className="border-y border-white/10 py-3">
<div className="-mx-1 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>
))}
</div>
</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">
<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">
{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"
>
<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="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{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>
</Link>
))}
</div>
</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) => (
<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"
>
<span className="min-w-0 truncate">{app.name}</span>
<span className="shrink-0 tabular-nums">{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>
<ScrollReveal delay={0.04}>
<UnlockSection />
</ScrollReveal>
</div>
);
}

37
app/pricing/page.tsx Normal file
View File

@@ -0,0 +1,37 @@
import Link from "next/link";
const plans = [
{ name: "Free", price: "0", desc: "基础下载与公开资源", features: ["基础下载源", "公开更新日志", "每日限量"] },
{ name: "Pro", price: "29", desc: "适合重度下载与创作者", features: ["高速下载源", "历史版本全集", "优先更新提醒"] },
{ name: "Ultra", price: "99", desc: "团队与运营场景", features: ["多端授权", "批量下载能力", "运营面板预留接口"] },
];
export default function PricingPage() {
return (
<div className="space-y-8 py-8">
<section className="text-center">
<h1 className="mt-2 text-4xl font-bold tracking-tight"></h1>
<p className="mx-auto mt-3 max-w-2xl text-sm text-slate-300"></p>
</section>
<section className="grid gap-4 md:grid-cols-3">
{plans.map((plan, idx) => (
<article key={plan.name} className={`card-glass ${idx === 1 ? "border-cyan-300/40" : ""}`}>
<p className="text-sm text-slate-300">{plan.name}</p>
<p className="mt-2 text-4xl font-bold">¥{plan.price}<span className="text-base text-slate-400">/</span></p>
<p className="mt-2 text-sm text-slate-300">{plan.desc}</p>
<ul className="mt-4 space-y-2 text-sm text-slate-200">
{plan.features.map((f) => <li key={f}>- {f}</li>)}
</ul>
<Link href={`/checkout?plan=${plan.name.toLowerCase()}&slug=nova-ai-studio`} className={`mt-5 block w-full text-center ${idx === 1 ? "cta-main" : "cta-sub"}`}>
</Link>
</article>
))}
</section>
<section className="card-glass flex flex-wrap items-center justify-between gap-3">
<p className="text-sm text-slate-300"></p>
<Link href="/unlock" className="cta-sub"></Link>
</section>
</div>
);
}

19
app/unlock/page.tsx Normal file
View File

@@ -0,0 +1,19 @@
import { QRCodeSection } from "@/components/app-detail";
import { UnlockWorkflow } from "@/components/unlock-workflow";
export default function UnlockPage() {
return (
<div className="space-y-6 py-8">
<h1 className="text-4xl font-bold tracking-tight"></h1>
<p className="max-w-2xl text-slate-300"></p>
<QRCodeSection />
<UnlockWorkflow />
<div className="card-glass space-y-2 text-sm text-slate-300">
<p>1) </p>
<p>2) </p>
<p>3) </p>
<p>4) 线</p>
</div>
</div>
);
}