This commit is contained in:
zhu
2026-05-07 16:14:31 +08:00
parent 95e99ec802
commit c661f4a063
151 changed files with 22280 additions and 4717 deletions

View File

@@ -0,0 +1,64 @@
import Link from "next/link";
export function Footer() {
const year = new Date().getFullYear();
return (
<footer className="mt-20 border-t border-border/60 bg-muted/30">
<div className="container mx-auto grid max-w-6xl grid-cols-2 gap-8 px-4 py-10 md:grid-cols-4 md:px-6">
<div className="col-span-2 md:col-span-1">
<div className="flex items-center gap-2 font-semibold tracking-tight">
<span className="inline-flex h-6 w-6 items-center justify-center rounded bg-foreground text-[11px] font-bold text-background">
S
</span>
StoreAI
</div>
<p className="mt-3 max-w-xs text-xs leading-relaxed text-muted-foreground">
AI store monitor - anomalies first, twice a day.
</p>
</div>
<FooterColumn title="Product">
<FooterLink href="/#how">How it works</FooterLink>
<FooterLink href="/pricing">Pricing</FooterLink>
<FooterLink href="/pricing#faq">FAQ</FooterLink>
<FooterLink href="/login">Sign in</FooterLink>
<FooterLink href="/signup">Start free</FooterLink>
</FooterColumn>
<FooterColumn title="Company">
<FooterLink href="mailto:hello@storeai.app">Contact</FooterLink>
</FooterColumn>
<FooterColumn title="Legal">
<FooterLink href="/terms">Terms</FooterLink>
<FooterLink href="/privacy-policy">Privacy</FooterLink>
</FooterColumn>
</div>
<div className="border-t border-border/60">
<div className="container mx-auto max-w-6xl px-4 py-4 text-xs text-muted-foreground md:px-6">
{year} StoreAI. All rights reserved.
</div>
</div>
</footer>
);
}
function FooterColumn({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div>
<div className="text-xs font-semibold uppercase tracking-wider text-foreground/80">
{title}
</div>
<div className="mt-3 flex flex-col gap-2">{children}</div>
</div>
);
}
function FooterLink({ href, children }: { href: string; children: React.ReactNode }) {
return (
<Link href={href} className="text-xs text-muted-foreground transition-colors hover:text-foreground">
{children}
</Link>
);
}

View File

@@ -0,0 +1,36 @@
import Link from "next/link";
export function Header() {
return (
<header className="sticky top-0 z-30 w-full border-b border-border/60 bg-background/80 backdrop-blur">
<div className="container mx-auto flex h-14 max-w-6xl items-center justify-between px-4 md:px-6">
<Link href="/" className="flex items-center gap-2 font-semibold tracking-tight">
<span className="inline-flex h-6 w-6 items-center justify-center rounded bg-foreground text-[11px] font-bold text-background">
S
</span>
StoreAI
</Link>
<nav className="flex items-center gap-1 text-sm">
<Link
href="/pricing"
className="rounded-md px-3 py-1.5 text-muted-foreground transition-colors hover:text-foreground"
>
Pricing
</Link>
<Link
href="/login"
className="rounded-md px-3 py-1.5 text-muted-foreground transition-colors hover:text-foreground"
>
Sign in
</Link>
<Link
href="/signup"
className="ml-1 rounded-md bg-foreground px-3 py-1.5 text-sm font-medium text-background transition-opacity hover:opacity-90"
>
Start free
</Link>
</nav>
</div>
</header>
);
}

View File

@@ -0,0 +1,257 @@
import {
Building2,
CreditCard,
Crosshair,
Eye,
FileText,
Inbox,
Layers,
PackageX,
Plug,
Settings,
Sparkles,
Star,
} from "lucide-react";
import type {
FaqContent,
HeroContent,
IconCardContent,
MetricContent,
PreviewPointContent,
PricingTierContent,
ReportPreviewContent,
SectionIntroContent,
WorkflowStepContent,
} from "./types";
/** 从原型迁移过来的首页静态内容,集中管理文案和展示用元数据。 */
export const HOME_HERO: HeroContent = {
eyebrow: "AI store monitor for ecommerce sellers",
title: "Catch what your store does between your check-ins.",
body:
"StoreAI watches your store twice a day, flags what's going wrong, and explains it in plain English. You don't need to babysit a dashboard.",
primaryCta: { label: "Start free", href: "/signup" },
secondaryCta: { label: "See how it works", href: "#how" },
footnote: "No credit card / Cancel anytime / Auto-pauses after the first day",
betaLabel: "Currently in private beta with founding sellers",
};
export const HOME_METRICS: MetricContent[] = [
{ id: "rules", number: "13", label: "anomaly rules built in" },
{ id: "scans", number: "2x", label: "scans per day" },
{ id: "delivery", number: "< 30s", label: "from anomaly detected to Telegram" },
];
export const PROBLEM_INTRO: SectionIntroContent = {
eyebrow: "The problem",
title: "You shouldn't learn about these from a customer.",
body:
"Every hour your store runs unsupervised, money walks out the door. The worst part: you find out too late.",
};
export const PROBLEM_CARDS: IconCardContent[] = [
{
id: "stockouts",
Icon: PackageX,
title: "Stock-outs go unnoticed",
body:
"Your bestseller goes to zero at 11 AM. You notice at 5 PM when a customer messages. Six hours of orders gone, and your search ranking dropped because the listing was unavailable.",
},
{
id: "ads",
Icon: CreditCard,
title: "Ads burn budget for nothing",
body:
"An auto-optimised campaign starts misfiring. Spend climbs, conversions tank. By the time you open Ads Center, the daily budget is gone and the report tells you yesterday's story.",
},
{
id: "competitors",
Icon: Eye,
title: "Competitors undercut you quietly",
body:
"A competitor drops their price 15% overnight. Your search position slides, sales dip, and you only investigate a week later when the dip looks like a trend.",
},
];
export const SOLUTION_INTRO: SectionIntroContent = {
eyebrow: "How StoreAI helps",
title: "A second pair of eyes that never blinks.",
body:
"StoreAI scans your store twice a day, runs every anomaly rule, and writes plain-English diagnoses. You get a focused report, not a dashboard to wade through.",
};
export const SOLUTION_CARDS: IconCardContent[] = [
{
id: "anomalies",
Icon: Crosshair,
title: "Anomalies first, always",
body:
"P1 issues like stockouts on listed SKUs and zero-conversion campaigns lead the report. Reference numbers come last, where they belong.",
},
{
id: "rules",
Icon: Layers,
title: "13 rules covering sales, ads, competitors",
body:
"Stock vs days-of-supply, GMV drops, top-SKU zero hours, ROAS thresholds, keyword concentration, competitor price gaps and shifts all running every scan.",
},
{
id: "plain-english",
Icon: FileText,
title: "Plain-English explanations",
body:
"Every alert ships with what it means, why it matters, and what to do next. No raw numbers, no rule IDs, no jargon.",
},
];
export const PREVIEW_INTRO: SectionIntroContent = {
eyebrow: "When it matters most",
title: "Anomalies first, before the routine numbers.",
body:
"Two reports a day, structured so the urgent stuff is at the top. Stockouts, ad waste, competitor moves, all surfaced where you'll actually notice them. The boring numbers are still there, just where they belong.",
};
export const PREVIEW_POINTS: PreviewPointContent[] = [
{ id: "first", text: "Issues sit on top of every report, so you see them in the first 5 seconds" },
{ id: "action", text: "Each alert ships with what to do, not just what happened" },
{ id: "competitor", text: "Competitor price moves get their own line, never lost in the noise" },
{ id: "recap", text: "Yesterday recap and today focus, side by side" },
];
export const REPORT_PREVIEW: ReportPreviewContent = {
headerLabel: "Morning brief / 25 Apr",
statusLabel: "New",
yesterdayLabel: "Yesterday",
yesterdayValue: "RM 5,376 / 30 orders / +12.2%",
issuesTitle: "3 issues to attend to today",
alerts: [
{
id: "stockout",
tag: "Urgent",
tone: "urgent",
title: "Out of stock but listed",
body: "VICTOR Badminton Short R-49217 shows 0 stock. Orders placed now will fail fulfilment.",
action: "Unpublish or arrange urgent restock",
},
{
id: "ad-roas",
tag: "Warning",
tone: "warning",
title: "Ad ROAS dropped to 1.4",
body: "Group Ads - Rackets target was 4.0. RM 280 spent yesterday for 6 orders.",
action: "Pause and review keyword performance",
},
],
competitorTitle: "Competitors moved",
competitorMove: "FOXER bag dropped to RM 4.91 (-8.3%)",
footer: "Open dashboard for the full picture",
};
export const WORKFLOW_INTRO: SectionIntroContent = {
eyebrow: "How it works",
title: "Up and running in 5 minutes. Two reports a day, every day after.",
body:
"No data integration, no IT involvement, no migration. The extension scans inside your own logged-in browser session, so your password never touches our servers.",
};
export const WORKFLOW_STEPS: WorkflowStepContent[] = [
{
id: "install",
number: 1,
Icon: Plug,
eyebrow: "Step 1 / 60 seconds",
title: "Sign up + install",
body: "Create your account. Install the Chrome extension from your dashboard.",
},
{
id: "configure",
number: 2,
Icon: Settings,
eyebrow: "Step 2 / 2 minutes",
title: "Pair + configure",
body:
"One-click pair the extension to your store. Set your morning and evening times. Add competitor URLs to watch.",
},
{
id: "receive",
number: 3,
Icon: Inbox,
eyebrow: "Step 3 / forever after",
title: "Receive + act",
body: "Twice a day, a report arrives. Skim it. Act on what matters. Get back to running your business.",
},
];
export const PRICING_INTRO: SectionIntroContent = {
eyebrow: "Pricing",
title: "Try it free for a day. Decide after that.",
body:
"One full day of StoreAI on us. If it doesn't catch something useful, walk away. If it does, $500 a month, one brand.",
};
export const PRICING_TIERS: PricingTierContent[] = [
{
id: "free",
Icon: Sparkles,
name: "Free trial",
price: "$0",
unit: "1 day",
features: ["Full access for 24 hours", "All anomaly rules included", "No credit card required"],
cta: { label: "Start free", href: "/signup" },
},
{
id: "standard",
Icon: Star,
name: "Standard",
price: "$500",
unit: "/ month",
note: "Billed monthly / cancel anytime / 1 brand included",
badge: "Most sellers",
isHighlighted: true,
features: ["Twice-daily scans + reports", "13 anomaly rules", "Up to 10 competitors watched"],
cta: { label: "Get started", href: "/signup?next=/dashboard/billing" },
},
{
id: "enterprise",
Icon: Building2,
name: "Enterprise",
price: "Custom",
unit: "",
features: ["Multiple brands under one account", "Custom anomaly rules", "Priority support + onboarding"],
cta: { label: "Contact us", href: "mailto:hello@storeai.app?subject=Enterprise%20enquiry" },
},
];
export const FAQ_INTRO: SectionIntroContent = {
eyebrow: "Common questions",
title: "Things people ask before signing up.",
};
export const FAQ_ROWS: FaqContent[] = [
{
id: "platform",
question: "Will the platform notice and ban my account?",
answer:
"No. The extension runs inside your own logged-in browser session, the same way you check the seller dashboard yourself. We never log in for you and never store your password.",
},
{
id: "time",
question: "I don't have time to read two reports a day.",
answer:
"You don't have to. Issues are at the top, so skim the urgent items in five seconds and ignore the rest. The boring numbers are there if you want them.",
},
{
id: "cancel",
question: "Can I cancel anytime?",
answer:
"Yes. Dashboard to Billing to Manage subscription. Stripe handles cancellation. You're covered to the end of the period and never billed again.",
},
];
export const FINAL_CTA: SectionIntroContent & { cta: HeroContent["primaryCta"] } = {
eyebrow: "",
title: "See your store the way StoreAI sees it.",
body: "Sign up, run one scan, read one report. If you're still on the fence after that, fair enough.",
cta: { label: "Start free", href: "/signup" },
};

View File

@@ -0,0 +1,88 @@
import { AlertTriangle, Clock, Crosshair } from "lucide-react";
import { REPORT_PREVIEW } from "./home-mock";
import type { ReportAlertContent } from "./types";
/**
* 首页里的产品报告预览卡。
* 当前只是静态界面原型,后续接入真实扫描数据时应替换预览数据源。
*/
export function ReportPreview() {
return (
<div className="marketing-lift mx-auto w-full max-w-md rounded-2xl border border-border bg-card p-6 shadow-xl">
<div className="flex items-center justify-between border-b border-border/60 pb-4">
<div className="flex items-center gap-2">
<div className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-foreground text-[11px] font-bold text-background">
S
</div>
<div>
<div className="text-sm font-semibold tracking-tight">StoreAI</div>
<div className="text-[10.5px] text-muted-foreground">{REPORT_PREVIEW.headerLabel}</div>
</div>
</div>
<span className="rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-semibold text-emerald-800">
{REPORT_PREVIEW.statusLabel}
</span>
</div>
<div className="mt-5">
<div className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{REPORT_PREVIEW.yesterdayLabel}
</div>
<div className="mt-1.5 text-sm font-medium tabular-nums">
{REPORT_PREVIEW.yesterdayValue}
</div>
</div>
<div className="mt-5">
<div className="flex items-center gap-2">
<AlertTriangle className="h-3.5 w-3.5 text-amber-600" aria-hidden />
<div className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{REPORT_PREVIEW.issuesTitle}
</div>
</div>
<div className="mt-3 space-y-3">
{REPORT_PREVIEW.alerts.map((alert) => (
<PreviewAlert key={alert.id} alert={alert} />
))}
</div>
</div>
<div className="mt-5 border-t border-border/60 pt-4">
<div className="flex items-center gap-2">
<Crosshair className="h-3.5 w-3.5 text-foreground/80" aria-hidden />
<div className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{REPORT_PREVIEW.competitorTitle}
</div>
</div>
<div className="mt-2 text-sm">{REPORT_PREVIEW.competitorMove}</div>
</div>
<div className="mt-5 flex items-center justify-end gap-1 text-[11px] text-muted-foreground">
{REPORT_PREVIEW.footer}
</div>
</div>
);
}
/** 报告预览卡中的单条异常提醒。 */
function PreviewAlert({ alert }: { alert: ReportAlertContent }) {
const tagClassName =
alert.tone === "urgent"
? "rounded-full bg-red-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-red-700 ring-1 ring-red-200"
: "rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-amber-800 ring-1 ring-amber-200";
return (
<div className="rounded-md border border-border/80 bg-muted/20 p-3">
<div className="flex items-center gap-2">
<span className={tagClassName}>{alert.tag}</span>
</div>
<div className="mt-1.5 text-sm font-semibold tracking-tight">{alert.title}</div>
<p className="mt-1 text-[12.5px] leading-relaxed text-muted-foreground">{alert.body}</p>
<div className="mt-1.5 flex items-center gap-1 text-[12px] font-medium">
<Clock className="h-3 w-3" aria-hidden />
{alert.action}
</div>
</div>
);
}

View File

@@ -0,0 +1,54 @@
"use client";
import { type ReactNode, useEffect, useRef, useState } from "react";
/** 滚动显现组件的入参,延迟时间用来控制同一区块内元素的错峰显现。 */
interface RevealProps {
children: ReactNode;
delay?: number;
className?: string;
}
/** 首页专用的轻量滚动显现组件,首屏元素会立即显示以避免首屏闪烁。 */
export function Reveal({ children, delay = 0, className = "" }: RevealProps) {
const ref = useRef<HTMLDivElement | null>(null);
const [isShown, setIsShown] = useState(false);
useEffect(() => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
setIsShown(true);
return;
}
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
setIsShown(true);
observer.disconnect();
return;
}
}
},
{ threshold: 0.15, rootMargin: "0px 0px -50px 0px" },
);
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return (
<div
ref={ref}
data-reveal
data-shown={isShown ? "1" : "0"}
style={{ transitionDelay: `${delay}ms` }}
className={className}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,291 @@
import Link from "next/link";
import { ArrowRight, CheckCircle2, Sparkles } from "lucide-react";
import {
FAQ_INTRO,
FAQ_ROWS,
FINAL_CTA,
HOME_HERO,
HOME_METRICS,
PREVIEW_INTRO,
PREVIEW_POINTS,
PRICING_INTRO,
PRICING_TIERS,
PROBLEM_CARDS,
PROBLEM_INTRO,
SOLUTION_CARDS,
SOLUTION_INTRO,
WORKFLOW_INTRO,
WORKFLOW_STEPS,
} from "./home-mock";
import { ReportPreview } from "./report-preview";
import { Reveal } from "./reveal";
import {
FaqDisclosure,
IconCard,
MetricStat,
PricingTierCard,
SectionIntro,
WorkflowStepCard,
} from "./ui";
/** 首页网格卡片的统一错峰动画间隔。 */
const GRID_STAGGER_MS = 80;
/**
* 首屏区块,承载品牌定位、主转化按钮和内测状态提示。
*/
export function HeroSection() {
return (
<section className="relative isolate overflow-hidden">
<div className="hero-orb" aria-hidden="true" />
<div className="container relative mx-auto max-w-5xl px-4 pb-16 pt-20 text-center md:px-6 md:pb-24 md:pt-28">
<Reveal>
<div className="inline-flex items-center gap-2 rounded-full border border-border bg-background/80 px-3 py-1 text-xs font-medium text-muted-foreground backdrop-blur">
<Sparkles className="h-3 w-3" aria-hidden="true" />
{HOME_HERO.eyebrow}
</div>
</Reveal>
<Reveal delay={50}>
<h1 className="mt-6 text-balance text-4xl font-semibold tracking-tight sm:text-5xl md:text-6xl md:leading-[1.05]">
{HOME_HERO.title}
</h1>
</Reveal>
<Reveal delay={120}>
<p className="mx-auto mt-6 max-w-2xl text-balance text-base leading-relaxed text-muted-foreground md:text-lg">
{HOME_HERO.body}
</p>
</Reveal>
<Reveal delay={180}>
<div className="mt-10 flex flex-col items-center justify-center gap-3 sm:flex-row">
<PrimaryCta href={HOME_HERO.primaryCta.href}>{HOME_HERO.primaryCta.label}</PrimaryCta>
<Link
href={HOME_HERO.secondaryCta.href}
className="inline-flex h-11 items-center rounded-md border border-border bg-background px-6 text-sm font-medium transition-colors hover:bg-accent"
>
{HOME_HERO.secondaryCta.label}
</Link>
</div>
</Reveal>
<Reveal delay={220}>
<p className="mt-6 text-xs text-muted-foreground">{HOME_HERO.footnote}</p>
</Reveal>
<Reveal delay={260}>
<p className="mt-3 inline-flex items-center gap-1.5 rounded-full border border-border/50 bg-background/60 px-2.5 py-0.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/90 backdrop-blur">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden="true" />
{HOME_HERO.betaLabel}
</p>
</Reveal>
</div>
</section>
);
}
/** 首页顶部的数据条,用三个指标快速解释产品能力。 */
export function MetricsSection() {
return (
<section className="border-y border-border/60 bg-background">
<div className="container mx-auto grid max-w-5xl grid-cols-1 gap-8 px-4 py-12 text-center md:grid-cols-3 md:gap-4 md:px-6">
{HOME_METRICS.map((metric, index) => (
<Reveal key={metric.id} delay={stagger(index)}>
<MetricStat metric={metric} />
</Reveal>
))}
</div>
</section>
);
}
/** 用户痛点区块,用具体损失场景解释为什么需要监控。 */
export function ProblemSection() {
return (
<section className="bg-muted/20">
<div className="container mx-auto max-w-5xl px-4 py-20 md:px-6 md:py-24">
<Reveal>
<SectionIntro intro={PROBLEM_INTRO} />
</Reveal>
<div className="mt-12 grid grid-cols-1 gap-5 md:grid-cols-3">
{PROBLEM_CARDS.map((card, index) => (
<Reveal key={card.id} delay={stagger(index, 100)}>
<IconCard Icon={card.Icon} title={card.title} body={card.body} variant="problem" />
</Reveal>
))}
</div>
</div>
</section>
);
}
/** 解决方案区块,说明 StoreAI 覆盖的核心能力。 */
export function SolutionSection() {
return (
<section className="border-t border-border/60">
<div className="container mx-auto max-w-5xl px-4 py-20 md:px-6 md:py-24">
<Reveal>
<SectionIntro intro={SOLUTION_INTRO} />
</Reveal>
<div className="mt-12 grid grid-cols-1 gap-5 md:grid-cols-3">
{SOLUTION_CARDS.map((card, index) => (
<Reveal key={card.id} delay={stagger(index)}>
<IconCard Icon={card.Icon} title={card.title} body={card.body} />
</Reveal>
))}
</div>
</div>
</section>
);
}
/** 报告预览区块,左侧讲价值,右侧展示静态产品样貌。 */
export function PreviewSection() {
return (
<section className="border-t border-border/60 bg-muted/20">
<div className="container mx-auto max-w-6xl px-4 py-20 md:px-6 md:py-24">
<div className="grid grid-cols-1 gap-12 md:grid-cols-2 md:items-center md:gap-16">
<Reveal>
<div>
<SectionIntro intro={PREVIEW_INTRO} align="left" />
<ul className="mt-8 space-y-3">
{PREVIEW_POINTS.map((point) => (
<li key={point.id} className="flex items-start gap-3 text-sm">
<CheckCircle2
className="mt-0.5 h-4 w-4 flex-shrink-0 text-foreground"
aria-hidden="true"
/>
<span className="text-foreground/85">{point.text}</span>
</li>
))}
</ul>
</div>
</Reveal>
<Reveal delay={120}>
<ReportPreview />
</Reveal>
</div>
</div>
</section>
);
}
/** 三步工作流区块,解释用户从注册到收报告的路径。 */
export function WorkflowSection() {
return (
<section id="how" className="border-t border-border/60">
<div className="container mx-auto max-w-5xl px-4 py-20 md:px-6 md:py-24">
<Reveal>
<SectionIntro intro={WORKFLOW_INTRO} />
</Reveal>
<div className="mt-14 grid grid-cols-1 gap-12 md:grid-cols-3 md:gap-8">
{WORKFLOW_STEPS.map((step, index) => (
<Reveal key={step.id} delay={stagger(index, 120)}>
<WorkflowStepCard step={step} />
</Reveal>
))}
</div>
</div>
</section>
);
}
/** 首页定价预览区块,只展示套餐摘要并引导到完整定价页。 */
export function PricingSection() {
return (
<section className="border-t border-border/60 bg-muted/20">
<div className="container mx-auto max-w-6xl px-4 py-20 md:px-6 md:py-24">
<Reveal>
<SectionIntro intro={PRICING_INTRO} />
</Reveal>
<div className="mt-12 grid grid-cols-1 gap-5 lg:grid-cols-3">
{PRICING_TIERS.map((tier, index) => (
<Reveal key={tier.id} delay={stagger(index, 100)}>
<PricingTierCard tier={tier} />
</Reveal>
))}
</div>
<div className="mt-10 text-center">
<Link
href="/pricing"
className="inline-flex h-10 items-center gap-2 rounded-md border border-border bg-background px-5 text-sm font-medium transition-colors hover:bg-accent"
>
See full pricing
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
</section>
);
}
/** 常见问题预览区块,放置注册前最常见的三个疑问。 */
export function FaqSection() {
return (
<section className="border-t border-border/60">
<div className="container mx-auto max-w-3xl px-4 py-20 md:px-6 md:py-24">
<Reveal>
<SectionIntro intro={FAQ_INTRO} />
</Reveal>
<div className="mt-10 space-y-3">
{FAQ_ROWS.map((row, index) => (
<Reveal key={row.id} delay={stagger(index)}>
<FaqDisclosure question={row.question} answer={row.answer} />
</Reveal>
))}
</div>
<div className="mt-8 text-center">
<Link
href="/pricing#faq"
className="inline-flex items-center gap-1 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
>
See all questions
<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
</Link>
</div>
</div>
</section>
);
}
/** 页面底部最终转化区块,再次给出注册入口。 */
export function FinalCtaSection() {
return (
<section className="border-t border-border/60">
<div className="container mx-auto max-w-3xl px-4 py-20 text-center md:px-6 md:py-24">
<Reveal>
<h2 className="text-balance text-3xl font-semibold tracking-tight md:text-4xl">
{FINAL_CTA.title}
</h2>
{FINAL_CTA.body && <p className="mt-4 text-base text-muted-foreground">{FINAL_CTA.body}</p>}
<PrimaryCta href={FINAL_CTA.cta.href} className="mt-8">
{FINAL_CTA.cta.label}
</PrimaryCta>
</Reveal>
</div>
</section>
);
}
/** 首页统一的主按钮样式,避免首屏和最终转化区出现视觉偏差。 */
function PrimaryCta({
href,
children,
className = "",
}: {
href: string;
children: React.ReactNode;
className?: string;
}) {
return (
<Link
href={href}
className={`${className} inline-flex h-11 items-center gap-2 rounded-md bg-foreground px-6 text-sm font-medium text-background transition-opacity hover:opacity-90`}
>
{children}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
);
}
/** 根据列表下标计算卡片显现动画延迟。 */
function stagger(index: number, step = GRID_STAGGER_MS) {
return index * step;
}

View File

@@ -0,0 +1,109 @@
import type { LucideIcon } from "lucide-react";
/** 首页模块统一使用的图标类型,当前来自项目图标库。 */
export type MarketingIcon = LucideIcon;
/** 首屏主视觉的完整文案结构,避免关键转化文案散落在页面结构中。 */
export interface HeroContent {
eyebrow: string;
title: string;
body: string;
primaryCta: LinkContent;
secondaryCta: LinkContent;
footnote: string;
betaLabel: string;
}
/** 页面内链接或按钮链接的基础结构。 */
export interface LinkContent {
label: string;
href: string;
}
/** 各个大区块共用的标题、眉标和说明文案。 */
export interface SectionIntroContent {
eyebrow: string;
title: string;
body?: string;
}
/** 数据条里的单个指标项。 */
export interface MetricContent {
id: string;
number: string;
label: string;
}
/** 带图标卡片的内容结构,服务问题卡片和方案卡片。 */
export interface IconCardContent {
id: string;
Icon: MarketingIcon;
title: string;
body: string;
}
/** 预览区左侧卖点列表的单行内容。 */
export interface PreviewPointContent {
id: string;
text: string;
}
/** 工作流程区块里的步骤卡片内容。 */
export interface WorkflowStepContent extends IconCardContent {
number: number;
eyebrow: string;
}
/** 定价卡片内容,包含视觉高亮和转化入口信息。 */
export interface PricingTierContent {
id: string;
Icon: MarketingIcon;
name: string;
price: string;
unit: string;
note?: string;
badge?: string;
isHighlighted?: boolean;
features: string[];
cta: LinkContent;
}
/** 常见问题折叠项内容。 */
export interface FaqContent {
id: string;
question: string;
answer: string;
}
/** 报告预览卡里的单条异常提醒。 */
export interface ReportAlertContent {
id: string;
tag: string;
tone: "urgent" | "warning";
title: string;
body: string;
action: string;
}
/** 报告预览卡的静态展示内容。 */
export interface ReportPreviewContent {
headerLabel: string;
statusLabel: string;
yesterdayLabel: string;
yesterdayValue: string;
issuesTitle: string;
alerts: ReportAlertContent[];
competitorTitle: string;
competitorMove: string;
footer: string;
}
/** 首页通用图标卡片的渲染参数。 */
export interface IconCardProps {
Icon: MarketingIcon;
title: string;
body: string;
variant?: "default" | "problem";
}

View File

@@ -0,0 +1,172 @@
import Link from "next/link";
import {CheckCircle2} from "lucide-react";
import {
IconCardProps,
MetricContent, PricingTierContent,
SectionIntroContent, WorkflowStepContent,
} from "./types";
/** 首页区块上方的小号眉标文字。 */
export function Eyebrow({children}: { children: React.ReactNode }) {
return (
<div className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
{children}
</div>
);
}
/**
* 首页复用的区块标题结构。
* 多个区块共享同一套标题层级,集中在这里可以避免字号和间距漂移。
*/
export function SectionIntro({
intro,
align = "center",
}: {
intro: SectionIntroContent;
align?: "center" | "left";
}) {
const wrapperClassName =
align === "center" ? "mx-auto max-w-2xl text-center" : "max-w-2xl text-left";
return (
<div className={wrapperClassName}>
{intro.eyebrow && <Eyebrow>{intro.eyebrow}</Eyebrow>}
<h2 className="mt-3 text-balance text-3xl font-semibold tracking-tight md:text-4xl">
{intro.title}
</h2>
{intro.body && (
<p className="mt-4 text-balance text-base text-muted-foreground">{intro.body}</p>
)}
</div>
);
}
/** 数据条中的单个指标展示。 */
export function MetricStat({metric}: { metric: MetricContent }) {
return (
<div>
<div className="text-3xl font-semibold tracking-tight tabular-nums md:text-4xl">
{metric.number}
</div>
<div className="mt-1 text-xs text-muted-foreground">{metric.label}</div>
</div>
);
}
/** 首页复用的图标说明卡,支持问题区和方案区两种密度。 */
export function IconCard({Icon, title, body, variant = "default"}: IconCardProps) {
const iconSize = variant === "problem" ? "md" : "sm";
const bodyClassName =
variant === "problem"
? "mt-3 text-sm leading-relaxed text-muted-foreground"
: "mt-2 text-sm leading-relaxed text-muted-foreground";
return (
<div className="marketing-lift h-full rounded-lg border border-border bg-card p-6 shadow-sm">
<IconBubble Icon={Icon} size={iconSize}/>
<h3 className="mt-4 text-base font-semibold tracking-tight">{title}</h3>
<p className={bodyClassName}>{body}</p>
</div>
);
}
/** 工作流程区块的单个步骤卡片。 */
export function WorkflowStepCard({step}: { step: WorkflowStepContent }) {
return (
<div className="relative">
<div className="flex items-center gap-3">
<div
className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-foreground bg-card font-semibold tabular-nums">
{step.number}
</div>
<div className="text-[10.5px] font-semibold uppercase tracking-widest text-muted-foreground">
{step.eyebrow}
</div>
</div>
<div className="mt-5">
<IconBubble Icon={step.Icon} size="sm"/>
</div>
<h3 className="mt-3 text-base font-semibold tracking-tight">{step.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{step.body}</p>
</div>
);
}
/** 定价区的单个套餐卡片,包含高亮态和转化入口。 */
export function PricingTierCard({tier}: { tier: PricingTierContent }) {
const TierIcon = tier.Icon;
const cardClassName = tier.isHighlighted
? "marketing-lift relative flex h-full flex-col rounded-2xl border-2 border-foreground bg-card p-7 shadow-lg"
: "marketing-lift relative flex h-full flex-col rounded-2xl border border-border bg-card p-7 shadow-sm";
const ctaClassName = tier.isHighlighted
? "mt-6 inline-flex h-10 w-full items-center justify-center rounded-md bg-foreground px-5 text-sm font-medium text-background transition-opacity hover:opacity-90"
: "mt-6 inline-flex h-10 w-full items-center justify-center rounded-md border border-border bg-background px-5 text-sm font-medium transition-colors hover:bg-accent";
return (
<div className={cardClassName}>
{tier.badge && (
<div
className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-foreground px-3 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-background">
{tier.badge}
</div>
)}
<div className="flex items-center gap-2">
<TierIcon className="h-4 w-4 text-foreground/80" aria-hidden/>
<span className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
{tier.name}
</span>
</div>
<div className="mt-4 flex items-end gap-2">
<span className="text-4xl font-semibold tracking-tight">{tier.price}</span>
{tier.unit && <span className="pb-1 text-sm text-muted-foreground">{tier.unit}</span>}
</div>
{tier.note && <p className="mt-2 text-[11px] text-muted-foreground">{tier.note}</p>}
<ul className="mt-5 flex-1 space-y-2.5">
{tier.features.map((feature) => (
<li key={feature} className="flex items-start gap-2.5 text-sm">
<CheckCircle2 className="mt-0.5 h-4 w-4 flex-shrink-0 text-foreground/80" aria-hidden/>
<span className="text-foreground/85">{feature}</span>
</li>
))}
</ul>
<Link href={tier.cta.href} className={ctaClassName}>
{tier.cta.label}
</Link>
</div>
);
}
/** 常见问题区块的折叠问答项。 */
export function FaqDisclosure({question, answer}: { question: string; answer: string }) {
return (
<details className="group rounded-md border border-border bg-card p-5 shadow-sm">
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 text-sm font-medium">
<span>{question}</span>
<span
className="text-muted-foreground transition-transform group-open:rotate-180"
aria-hidden="true"
>
v
</span>
</summary>
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">{answer}</p>
</details>
);
}
/** 卡片内部统一的小图标容器,保证所有图标尺寸和底色一致。 */
function IconBubble({Icon, size}: { Icon: IconCardProps["Icon"]; size: "sm" | "md" }) {
const boxClassName =
size === "md"
? "inline-flex h-10 w-10 items-center justify-center rounded-md bg-foreground/[0.04] text-foreground/80 ring-1 ring-foreground/[0.06]"
: "inline-flex h-9 w-9 items-center justify-center rounded-md bg-foreground/[0.04] text-foreground/80 ring-1 ring-foreground/[0.06]";
const iconClassName = size === "md" ? "h-5 w-5" : "h-[18px] w-[18px]";
return (
<div className={boxClassName}>
<Icon className={iconClassName} aria-hidden/>
</div>
);
}

50
src/app/(home)/index.scss Normal file
View File

@@ -0,0 +1,50 @@
.storeai-home {
.hero-orb {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(60% 50% at 50% 0%, rgb(2 8 23 / 0.06) 0%, transparent 60%),
radial-gradient(40% 40% at 80% 60%, rgb(2 8 23 / 0.04) 0%, transparent 70%),
radial-gradient(40% 40% at 20% 70%, rgb(2 8 23 / 0.04) 0%, transparent 70%);
}
[data-reveal] {
opacity: 0;
transform: translateY(12px);
transition: opacity 700ms ease-out,
transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
will-change: opacity, transform;
}
[data-reveal][data-shown="1"] {
opacity: 1;
transform: translateY(0);
}
.marketing-lift {
transition: transform 200ms ease-out,
box-shadow 200ms ease-out,
border-color 200ms ease-out;
&:hover {
transform: translateY(-2px);
box-shadow: 0 12px 24px -12px rgb(0 0 0 / 0.18),
0 2px 6px -2px rgb(0 0 0 / 0.08);
}
}
}
@media (prefers-reduced-motion: reduce) {
.storeai-home {
[data-reveal] {
opacity: 1;
transform: none;
transition: none;
}
.marketing-lift,
.marketing-lift:hover {
transform: none;
}
}
}

34
src/app/(home)/page.tsx Normal file
View File

@@ -0,0 +1,34 @@
import {Footer} from "./components/footer";
import {Header} from "./components/header";
import "./index.scss";
import {
FaqSection, FinalCtaSection,
HeroSection,
MetricsSection,
PreviewSection, PricingSection,
ProblemSection,
SolutionSection,
WorkflowSection
} from "./components/home/sections";
export default function Home() {
return (
<div className="flex min-h-screen flex-col">
<Header/>
<div className="flex-1">
<main className="storeai-home">
<HeroSection/>
<MetricsSection/>
<ProblemSection/>
<SolutionSection/>
<PreviewSection/>
<WorkflowSection/>
<PricingSection/>
<FaqSection/>
<FinalCtaSection/>
</main>
</div>
<Footer/>
</div>
);
}

49
src/app/globals.css Normal file
View File

@@ -0,0 +1,49 @@
@import "tailwindcss";
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-border: var(--border);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
}
:root {
--background: #ffffff;
--foreground: #020817;
--card: #ffffff;
--muted: #f1f5f9;
--muted-foreground: #64748b;
--accent: #f1f5f9;
--border: #e2e8f0;
--radius: 0.5rem;
}
* {
box-sizing: border-box;
border-color: var(--border);
}
body {
margin: 0;
background: var(--background);
color: var(--foreground);
font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
font-feature-settings: "rlig" 1, "calt" 1;
}
a {
text-decoration: none;
}
button,
input,
textarea,
select {
font: inherit;
}

33
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,33 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "StoreAI",
description: "AI store monitor for ecommerce sellers.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
}