Merge pull request 'feat: Smooth 5-second auto-rotation for Hero stories and ProductShowcase cards' (#1) from feature/smooth-image-rotation into main
This commit is contained in:
commit
010231a4fe
12
app/page.tsx
12
app/page.tsx
@ -1,16 +1,18 @@
|
|||||||
import { Hero } from "@/components/marketing/Hero";
|
import { Hero } from "@/components/marketing/Hero";
|
||||||
import { Navbar } from "@/components/marketing/Navbar";
|
import { Navbar } from "@/components/marketing/Navbar";
|
||||||
import { CTA, FeatureGrid, PersonaGrid, Pricing } from "@/components/marketing/Sections";
|
import { FAQ, FooterCTA, Manifesto, Pricing, ProductShowcase, StatsBand } from "@/components/marketing/Sections";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main className="bg-[#0A0A09]">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<Hero />
|
<Hero />
|
||||||
<FeatureGrid />
|
<ProductShowcase />
|
||||||
<PersonaGrid />
|
<StatsBand />
|
||||||
|
<Manifesto />
|
||||||
<Pricing />
|
<Pricing />
|
||||||
<CTA />
|
<FAQ />
|
||||||
|
<FooterCTA />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,48 +1,126 @@
|
|||||||
import { ArrowRight, ShieldCheck, Sparkles } from "lucide-react";
|
"use client";
|
||||||
import { AreaChart } from "@/components/charts/AreaChart";
|
|
||||||
import { ButtonLink } from "@/components/ui/Button";
|
import { useEffect, useState } from "react";
|
||||||
import { Card, StatCard } from "@/components/ui/Card";
|
import { ArrowRight, RefreshCcw, Sparkles } from "lucide-react";
|
||||||
import { Chip } from "@/components/ui/Chip";
|
import Link from "next/link";
|
||||||
|
import { StoryBackdrop } from "@/components/marketing/StoryBackdrop";
|
||||||
|
import { randomStory, type GoalStory } from "@/lib/stories";
|
||||||
|
|
||||||
|
const askAnswers: [string, string][] = [
|
||||||
|
["Can I afford a $2,400 vacation in December?", "Yes — set aside $400/month starting now and your flex budget stays green the whole way."],
|
||||||
|
["When can I buy my first home?", "At your current savings pace, a 10% down payment on a $380k home is ~34 months out. Two changes could make it 26."],
|
||||||
|
["Which card should I use for groceries?", "Sapphire Preferred — 3x points beats your flat 2% card. Worth about $18 more this month."],
|
||||||
|
["When can I retire?", "Holding your 14% savings rate, a comfortable retirement at 63 is on track. Bump to 17% and it's 61."],
|
||||||
|
];
|
||||||
|
|
||||||
export function Hero() {
|
export function Hero() {
|
||||||
|
const [story, setStory] = useState<GoalStory | null>(null);
|
||||||
|
const [askIndex, setAskIndex] = useState(0);
|
||||||
|
const [asked, setAsked] = useState<[string, string] | null>(null);
|
||||||
|
const [input, setInput] = useState("");
|
||||||
|
const [fadeOut, setFadeOut] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStory(randomStory());
|
||||||
|
const t = setInterval(() => setAskIndex((i) => (i + 1) % askAnswers.length), 3500);
|
||||||
|
return () => clearInterval(t);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storyInterval = setInterval(() => {
|
||||||
|
setFadeOut(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setStory((s) => randomStory(s?.id));
|
||||||
|
setFadeOut(false);
|
||||||
|
}, 500);
|
||||||
|
}, 5000);
|
||||||
|
return () => clearInterval(storyInterval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const ask = () => {
|
||||||
|
const typed = input.trim().toLowerCase();
|
||||||
|
const match =
|
||||||
|
askAnswers.find(([q]) => typed && q.toLowerCase().includes(typed.slice(0, 12))) ??
|
||||||
|
askAnswers[askIndex];
|
||||||
|
setAsked(match);
|
||||||
|
setInput("");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="grain">
|
<section className="relative isolate flex min-h-screen flex-col overflow-hidden bg-[#0A0A09]">
|
||||||
<div className="mx-auto grid min-h-[calc(100vh-73px)] max-w-7xl items-center gap-10 px-5 py-14 lg:grid-cols-[1fr_.9fr]">
|
{story && <div className={`transition-opacity duration-500 ${fadeOut ? 'opacity-0' : 'opacity-100'}`}><StoryBackdrop story={story} dim="soft" /></div>}
|
||||||
<div>
|
{/* extra cinematic vignette */}
|
||||||
<Chip>Subscription-only · zero ads · zero referral bias</Chip>
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(85%_70%_at_50%_35%,transparent_40%,rgba(10,10,9,.75)_100%)]" />
|
||||||
<h1 className="mt-6 max-w-3xl font-display text-6xl leading-[0.95] text-ink dark:text-paper md:text-7xl">
|
|
||||||
Your money app should help you move, not just watch.
|
<div className="relative mx-auto flex w-full max-w-4xl flex-1 flex-col items-center justify-center px-5 pb-40 pt-32 text-center">
|
||||||
</h1>
|
<p className="font-mono text-[11px] uppercase tracking-[.3em] text-mint">
|
||||||
<p className="mt-6 max-w-2xl text-lg leading-8 text-ink/68 dark:text-paper/68">
|
Subscription-only · Zero ads · Zero referral bias
|
||||||
Aartha unifies budgets, cash flow, credit, rewards, goals, and AI coaching into one calm finance cockpit built around the user's wellbeing.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8 flex flex-wrap gap-3">
|
<h1 className="mt-6 font-display text-6xl leading-[1.02] text-paper md:text-[84px]">
|
||||||
<ButtonLink href="/dashboard">View dashboard <ArrowRight size={17} /></ButtonLink>
|
<em className="text-mint">Own</em> your story.
|
||||||
<ButtonLink href="/mobile" variant="secondary">Mobile screens</ButtonLink>
|
</h1>
|
||||||
|
<p className="mt-5 max-w-xl text-lg leading-8 text-paper/80">
|
||||||
|
Aartha is the personal finance app that works only for you — budgets, credit,
|
||||||
|
rewards, and an AI coach pointed at the life you're building.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Ask Aartha — interactive, right in the hero */}
|
||||||
|
<div className="mt-9 w-full max-w-xl">
|
||||||
|
<div className="flex items-center gap-2 rounded-full border border-white/15 bg-[#0A0A09]/60 p-2 pl-5 backdrop-blur-md transition focus-within:border-mint/60">
|
||||||
|
<Sparkles size={16} className="flex-none text-mint" />
|
||||||
|
<input
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && ask()}
|
||||||
|
placeholder={askAnswers[askIndex][0]}
|
||||||
|
className="w-full bg-transparent text-sm text-paper placeholder:text-paper/45 focus:outline-none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={ask}
|
||||||
|
className="flex-none rounded-full bg-paper px-4 py-2.5 font-mono text-[11px] font-bold uppercase tracking-[.15em] text-ink transition hover:bg-mint"
|
||||||
|
>
|
||||||
|
Ask
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 flex flex-wrap gap-5 text-sm font-semibold text-ink/60 dark:text-paper/60">
|
<p className="mt-3 font-mono text-[10px] uppercase tracking-[.25em] text-paper/40">
|
||||||
<span className="inline-flex items-center gap-2"><ShieldCheck size={16} /> Bank-grade posture</span>
|
Track everything · Ask anything · Trust the answer
|
||||||
<span className="inline-flex items-center gap-2"><Sparkles size={16} /> Ask Aartha AI coach</span>
|
</p>
|
||||||
|
{asked && (
|
||||||
|
<div className="mt-4 animate-fadeUp rounded-lg border border-white/12 bg-[#0A0A09]/70 p-4 text-left backdrop-blur-md">
|
||||||
|
<p className="font-mono text-[10px] uppercase tracking-[.2em] text-paper/45">{asked[0]}</p>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-paper/90">{asked[1]}</p>
|
||||||
|
<p className="mt-2 text-[10px] text-paper/35">Sample answer · Educational guidance only, not regulated financial advice.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-9 flex items-center gap-3">
|
||||||
|
<Link href="/dashboard" className="inline-flex items-center gap-2 rounded-full bg-mint px-6 py-3.5 text-sm font-bold text-ink transition hover:bg-paper">
|
||||||
|
Get started <ArrowRight size={16} />
|
||||||
|
</Link>
|
||||||
|
<a href="#product" className="rounded-full border border-white/20 px-6 py-3.5 text-sm font-semibold text-paper/85 transition hover:border-white/50 hover:text-paper">
|
||||||
|
See the product
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Card className="p-5">
|
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
{/* Story caption — the human moment behind this visit */}
|
||||||
<StatCard label="Net worth" value="$184k" detail="+8.2% YTD" />
|
{story && (
|
||||||
<StatCard label="Flex left" value="$93" detail="Safe to spend today" tone="saffron" />
|
<div className={`absolute inset-x-0 bottom-0 z-10 border-t border-white/10 bg-gradient-to-t from-[#0A0A09] to-transparent transition-opacity duration-500 ${fadeOut ? 'opacity-0' : 'opacity-100'}`}>
|
||||||
<StatCard label="FICO 8" value="742" detail="+21 since April" tone="indigo" />
|
<div className="mx-auto flex max-w-7xl flex-wrap items-end justify-between gap-3 px-5 py-5">
|
||||||
</div>
|
|
||||||
<div className="mt-5 rounded-lg bg-white/70 p-5 dark:bg-white/[0.05]">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-bold uppercase tracking-wide text-ink/45 dark:text-paper/45">Cash-flow forecast</p>
|
<p className="font-mono text-[10px] uppercase tracking-[.25em] text-mint">{story.emoji} {story.tag} — a different story every visit</p>
|
||||||
<p className="font-display text-4xl text-forest dark:text-mint">$7,420</p>
|
<p className="mt-1 font-display text-xl text-paper">{story.headline}</p>
|
||||||
</div>
|
</div>
|
||||||
<Chip tone="saffron">Next 30 days</Chip>
|
<button
|
||||||
|
onClick={() => setStory((s) => randomStory(s?.id))}
|
||||||
|
className="inline-flex items-center gap-2 rounded-full border border-white/15 px-4 py-2 font-mono text-[10px] uppercase tracking-[.2em] text-paper/60 transition hover:border-white/40 hover:text-paper"
|
||||||
|
>
|
||||||
|
<RefreshCcw size={11} /> Another story
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<AreaChart values={[8, 12, 11, 19, 18, 24, 31, 28, 36]} />
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,29 @@
|
|||||||
import { ButtonLink } from "@/components/ui/Button";
|
import Link from "next/link";
|
||||||
import { Logo } from "@/components/ui/Logo";
|
import { LeafMark } from "@/components/ui/Logo";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal cinematic nav — two destinations, two actions. Nothing else.
|
||||||
|
*/
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-40 border-b border-forest/10 bg-cream/80 backdrop-blur-xl dark:border-white/10 dark:bg-[#08130f]/80">
|
<header className="fixed inset-x-0 top-0 z-40">
|
||||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-5 py-4">
|
<div className="mx-auto flex max-w-7xl items-center justify-between px-5 py-4">
|
||||||
<Logo />
|
<Link href="/" className="flex items-center gap-2.5 font-display text-2xl text-paper">
|
||||||
<nav className="hidden items-center gap-7 text-sm font-semibold text-ink/70 dark:text-paper/70 md:flex">
|
<LeafMark size={26} /> Aartha
|
||||||
<a href="#features">Features</a>
|
</Link>
|
||||||
<a href="#pricing">Pricing</a>
|
<nav className="hidden items-center gap-1 rounded-full bg-white/[.07] p-1 font-mono text-[11px] uppercase tracking-[.15em] text-paper/75 backdrop-blur-md md:flex">
|
||||||
<a href="/dashboard">Dashboard</a>
|
<a href="#product" className="rounded-full px-4 py-2 transition hover:bg-white/10 hover:text-paper">Product</a>
|
||||||
<a href="/mobile">Mobile</a>
|
<a href="#pricing" className="rounded-full px-4 py-2 transition hover:bg-white/10 hover:text-paper">Pricing</a>
|
||||||
|
<a href="#faq" className="rounded-full px-4 py-2 transition hover:bg-white/10 hover:text-paper">FAQ</a>
|
||||||
</nav>
|
</nav>
|
||||||
<ButtonLink href="/dashboard" size="sm">Open app</ButtonLink>
|
<div className="flex items-center gap-2">
|
||||||
|
<Link href="/dashboard" className="hidden rounded-full px-4 py-2 font-mono text-[11px] uppercase tracking-[.15em] text-paper/75 transition hover:text-paper sm:block">
|
||||||
|
Log in
|
||||||
|
</Link>
|
||||||
|
<Link href="/dashboard" className="rounded-full bg-paper px-4 py-2 font-mono text-[11px] font-bold uppercase tracking-[.15em] text-ink transition hover:bg-mint">
|
||||||
|
Get started →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,44 +1,100 @@
|
|||||||
import { personas, prices } from "@/lib/data";
|
"use client";
|
||||||
import { Card } from "@/components/ui/Card";
|
|
||||||
import { Chip } from "@/components/ui/Chip";
|
|
||||||
import { SectionHeading } from "@/components/ui/SectionHeading";
|
|
||||||
import { ButtonLink } from "@/components/ui/Button";
|
|
||||||
|
|
||||||
const features = [
|
import { useEffect, useState } from "react";
|
||||||
["Flex budgeting", "Speedometer-style daily allowance after fixed expenses and goals."],
|
import Link from "next/link";
|
||||||
["Credit clarity", "Real FICO 8 factors translated into actions users can actually take."],
|
import { ArrowRight, Check, Minus, Plus } from "lucide-react";
|
||||||
["Rewards intelligence", "Recommend the best card for each purchase based on live transactions."],
|
import { GaugeChart } from "@/components/charts/GaugeChart";
|
||||||
["Shared finances", "Partner and roommate views with privacy-aware collaboration."]
|
import { ProgressBar } from "@/components/ui/ProgressBar";
|
||||||
];
|
import { CountUp } from "@/components/ui/CountUp";
|
||||||
|
import { LeafMark } from "@/components/ui/Logo";
|
||||||
|
import { prices } from "@/lib/data";
|
||||||
|
|
||||||
export function FeatureGrid() {
|
const mono = "font-mono text-[10px] uppercase tracking-[.25em]";
|
||||||
return (
|
|
||||||
<section id="features" className="mx-auto max-w-7xl px-5 py-24">
|
function Eyebrow({ children }: { children: string }) {
|
||||||
<SectionHeading eyebrow="Product system" title="A PFM that feels active, honest, and useful." body="The MVP and differentiators are designed as reusable product modules, not one-off pages." />
|
return <p className={`${mono} text-mint`}>{children}</p>;
|
||||||
<div className="mt-12 grid gap-4 md:grid-cols-4">
|
|
||||||
{features.map(([title, body]) => (
|
|
||||||
<Card key={title}>
|
|
||||||
<h3 className="text-lg font-bold text-ink dark:text-paper">{title}</h3>
|
|
||||||
<p className="mt-3 text-sm leading-6 text-ink/62 dark:text-paper/62">{body}</p>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PersonaGrid() {
|
/* ---------- Product showcase — live UI, not screenshots ---------- */
|
||||||
|
export function ProductShowcase() {
|
||||||
|
const [activeCard, setActiveCard] = useState(0);
|
||||||
|
const cardCount = 3;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setActiveCard((i) => (i + 1) % cardCount);
|
||||||
|
}, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{
|
||||||
|
title: "Flex budget",
|
||||||
|
mono: "Flex budget",
|
||||||
|
label: "$93 safe to spend today",
|
||||||
|
content: <GaugeChart value={72} label="$93 safe to spend today" />,
|
||||||
|
heading: "One number, not thirty categories.",
|
||||||
|
description: "Income − fixed costs − goals. What's left is truly yours to spend.",
|
||||||
|
gradient: "from-[#123B2E] to-[#0C1512]",
|
||||||
|
bgLight: "bg-[#0E1F19]/80"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Guardrails",
|
||||||
|
mono: "Category guardrails",
|
||||||
|
label: "Warnings before the overspend",
|
||||||
|
content: <div className="space-y-5">
|
||||||
|
<ProgressBar label="Groceries" value={84} />
|
||||||
|
<ProgressBar label="Dining" value={109} />
|
||||||
|
<ProgressBar label="Transport" value={42} />
|
||||||
|
</div>,
|
||||||
|
heading: "Warnings before the overspend.",
|
||||||
|
description: "Green under 80%, amber to 100%, red past it — and a nudge before it happens.",
|
||||||
|
gradient: "from-[#3B2E12] to-[#15120C]",
|
||||||
|
bgLight: "bg-[#1F1A0E]/80"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Credit",
|
||||||
|
mono: "FICO 8 · real score",
|
||||||
|
label: "+21 in 90 days",
|
||||||
|
content: <>
|
||||||
|
<p className="mt-4 font-display text-7xl text-paper"><CountUp value={742} /></p>
|
||||||
|
<p className="mt-1 text-sm font-semibold text-mint">+21 in 90 days</p>
|
||||||
|
<p className="mt-4 text-sm leading-6 text-paper/55">“Pay $410 on your Chase card before the 28th to likely gain ~9 points.”</p>
|
||||||
|
</>,
|
||||||
|
heading: "Credit coaching with zero bias.",
|
||||||
|
description: "We never earn referral fees, so the advice is only ever for you.",
|
||||||
|
gradient: "from-[#221C46] to-[#100E1C]",
|
||||||
|
bgLight: "bg-[#161230]/80"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-paper/70 py-24 dark:bg-white/[0.03]">
|
<section id="product" className="dark border-t border-white/8 py-28">
|
||||||
<div className="mx-auto max-w-7xl px-5">
|
<div className="mx-auto max-w-7xl px-5">
|
||||||
<SectionHeading eyebrow="Audience" title="Built for the five PRD personas." body="Each persona maps to a feature surface and a distinct product promise." />
|
<Eyebrow>The product</Eyebrow>
|
||||||
<div className="mt-12 grid gap-4 md:grid-cols-5">
|
<h2 className="mt-4 max-w-2xl font-display text-5xl leading-[1.05] text-paper md:text-6xl">
|
||||||
{personas.map(([name, body], index) => (
|
Track everything. <em className="text-mint">Feel</em> everything.
|
||||||
<Card key={name} className="p-4">
|
</h2>
|
||||||
<Chip tone={index % 2 ? "indigo" : "mint"}>{String(index + 1).padStart(2, "0")}</Chip>
|
<p className="mt-5 max-w-xl text-paper/65">
|
||||||
<h3 className="mt-4 font-bold text-ink dark:text-paper">{name}</h3>
|
These aren't screenshots — they're the real components, running live.
|
||||||
<p className="mt-3 text-sm leading-6 text-ink/62 dark:text-paper/62">{body}</p>
|
Budgets that breathe, credit that explains itself, goals that feel close.
|
||||||
</Card>
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-14 grid gap-5 md:grid-cols-3">
|
||||||
|
{cards.map((card, idx) => (
|
||||||
|
<div
|
||||||
|
key={card.title}
|
||||||
|
className={`group overflow-hidden rounded-lg bg-gradient-to-b ${card.gradient} p-1 transition-all duration-500 hover:-translate-y-1 ${activeCard === idx ? 'ring-2 ring-mint opacity-100' : 'opacity-60 md:opacity-100'}`}
|
||||||
|
>
|
||||||
|
<div className={`rounded-[7px] ${card.bgLight} p-6 backdrop-blur`}>
|
||||||
|
<p className={`${mono} text-paper/45`}>{card.mono}</p>
|
||||||
|
<div className="mt-6">{card.content}</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-5">
|
||||||
|
<p className="font-bold text-paper">{card.heading}</p>
|
||||||
|
<p className="mt-1.5 text-sm leading-6 text-paper/55">{card.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -46,33 +102,160 @@ export function PersonaGrid() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Stats band ---------- */
|
||||||
|
export function StatsBand() {
|
||||||
|
const stats: [number, string, string, string][] = [
|
||||||
|
[100, "k+", "", "members across the US"],
|
||||||
|
[312, "", "$", "average saved per month"],
|
||||||
|
[4.8, "★", "", "member rating"],
|
||||||
|
[0, "", "$", "referral fees. Ever."],
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<section className="border-t border-white/8 py-20">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-10 px-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{stats.map(([value, suffix, prefix, label]) => (
|
||||||
|
<div key={label}>
|
||||||
|
<p className="font-display text-6xl text-paper">
|
||||||
|
<CountUp value={value} suffix={suffix} prefix={prefix} decimals={value % 1 ? 1 : 0} />
|
||||||
|
</p>
|
||||||
|
<p className={`mt-2 ${mono} text-paper/45`}>{label}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Manifesto ---------- */
|
||||||
|
export function Manifesto() {
|
||||||
|
return (
|
||||||
|
<section className="border-t border-white/8 py-28">
|
||||||
|
<div className="mx-auto max-w-4xl px-5 text-center">
|
||||||
|
<Eyebrow>Why Aartha exists</Eyebrow>
|
||||||
|
<h2 className="mt-6 font-display text-5xl leading-[1.08] text-paper md:text-6xl">
|
||||||
|
Every other money app is paid to point you somewhere.
|
||||||
|
<em className="text-mint"> We are paid by you — so we point at your goals.</em>
|
||||||
|
</h2>
|
||||||
|
<div className="mx-auto mt-10 flex max-w-2xl flex-wrap items-center justify-center gap-3">
|
||||||
|
{["No ads", "No referral fees", "No selling your data", "No dark patterns", "Cancel anytime"].map((t) => (
|
||||||
|
<span key={t} className="inline-flex items-center gap-2 rounded-full border border-white/12 px-4 py-2 text-sm text-paper/75">
|
||||||
|
<Check size={14} className="text-mint" /> {t}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Pricing ---------- */
|
||||||
export function Pricing() {
|
export function Pricing() {
|
||||||
return (
|
return (
|
||||||
<section id="pricing" className="mx-auto max-w-7xl px-5 py-24">
|
<section id="pricing" className="border-t border-white/8 py-28">
|
||||||
<SectionHeading eyebrow="Pricing" title="Clear subscription tiers. No hidden incentives." body="Pricing follows the PRD and reinforces Aartha's zero-conflict positioning." />
|
<div className="mx-auto max-w-7xl px-5">
|
||||||
|
<Eyebrow>Pricing</Eyebrow>
|
||||||
|
<h2 className="mt-4 max-w-2xl font-display text-5xl leading-[1.05] text-paper">
|
||||||
|
Honest tiers. No hidden incentives.
|
||||||
|
</h2>
|
||||||
<div className="mt-12 grid gap-4 md:grid-cols-4">
|
<div className="mt-12 grid gap-4 md:grid-cols-4">
|
||||||
{prices.map(([name, price, body]) => (
|
{prices.map(([name, price, body]) => {
|
||||||
<Card key={name} className={name === "Plus" ? "ring-2 ring-mint" : ""}>
|
const popular = name === "Plus";
|
||||||
{name === "Plus" ? <Chip tone="saffron">Most popular</Chip> : null}
|
return (
|
||||||
<h3 className="mt-4 text-xl font-bold text-ink dark:text-paper">{name}</h3>
|
<div
|
||||||
<p className="mt-2 font-display text-4xl text-forest dark:text-mint">{price}</p>
|
key={name}
|
||||||
<p className="mt-3 min-h-12 text-sm leading-6 text-ink/62 dark:text-paper/62">{body}</p>
|
className={`rounded-lg border p-6 transition hover:-translate-y-1 ${
|
||||||
<ButtonLink href="/dashboard" variant={name === "Plus" ? "primary" : "secondary"} className="mt-6 w-full">Preview tier</ButtonLink>
|
popular ? "border-mint bg-mint/[.06]" : "border-white/10 bg-white/[.03]"
|
||||||
</Card>
|
}`}
|
||||||
))}
|
>
|
||||||
|
{popular ? (
|
||||||
|
<p className={`${mono} text-mint`}>Most popular</p>
|
||||||
|
) : (
|
||||||
|
<p className={`${mono} text-paper/35`}> </p>
|
||||||
|
)}
|
||||||
|
<h3 className="mt-3 text-lg font-bold text-paper">{name}</h3>
|
||||||
|
<p className="mt-1 font-display text-5xl text-paper">{price}</p>
|
||||||
|
<p className="mt-3 min-h-12 text-sm leading-6 text-paper/55">{body}</p>
|
||||||
|
<Link
|
||||||
|
href="/dashboard"
|
||||||
|
className={`mt-6 flex items-center justify-center gap-2 rounded-full py-2.5 text-sm font-bold transition ${
|
||||||
|
popular ? "bg-mint text-ink hover:bg-paper" : "border border-white/15 text-paper/85 hover:border-white/40"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Get started <ArrowRight size={14} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CTA() {
|
/* ---------- FAQ ---------- */
|
||||||
|
const faqs: [string, string][] = [
|
||||||
|
["What does subscription-only actually mean?", "Your subscription is our only revenue. No ads, no referral fees, no selling your data. When Aartha recommends a card or an action, there is no financial incentive behind it — that's structural, not a promise."],
|
||||||
|
["Is my bank data safe?", "Connections run through Plaid and MX with bank-grade encryption. Aartha never sees or stores your bank credentials, and your data is never sold or shared."],
|
||||||
|
["Is this real financial advice?", "Aartha's AI coach provides educational financial information, not regulated financial advice. It explains what it noticed, why it matters, and a suggested action — you decide."],
|
||||||
|
["What happens if I cancel?", "Cancel in two taps — no guilt screens. Your data stays exportable for 90 days, then it's deleted. We tell you the exact date."],
|
||||||
|
["How is this different from Monarch or Credit Karma?", "Monarch shows you your money; Credit Karma is paid to recommend products. Aartha actively coaches you toward your goals and is structurally unable to profit from biased advice."],
|
||||||
|
];
|
||||||
|
|
||||||
|
export function FAQ() {
|
||||||
|
const [open, setOpen] = useState<number | null>(0);
|
||||||
return (
|
return (
|
||||||
<section className="px-5 pb-24">
|
<section id="faq" className="border-t border-white/8 py-28">
|
||||||
<div className="mx-auto max-w-7xl rounded-lg bg-forest p-10 text-paper dark:bg-mint dark:text-ink md:p-14">
|
<div className="mx-auto max-w-3xl px-5">
|
||||||
<h2 className="font-display text-5xl">Ready for a component-based handoff.</h2>
|
<Eyebrow>FAQ</Eyebrow>
|
||||||
<p className="mt-4 max-w-2xl text-paper/75 dark:text-ink/70">This prototype is structured for product review today and engineering continuation tomorrow.</p>
|
<h2 className="mt-4 font-display text-5xl text-paper">Clear answers.</h2>
|
||||||
<ButtonLink href="/dashboard" variant="secondary" className="mt-8">Explore app prototype</ButtonLink>
|
<div className="mt-10 divide-y divide-white/8 border-y border-white/8">
|
||||||
|
{faqs.map(([q, a], i) => (
|
||||||
|
<div key={q}>
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(open === i ? null : i)}
|
||||||
|
className="flex w-full items-center justify-between gap-4 py-5 text-left"
|
||||||
|
>
|
||||||
|
<span className="font-semibold text-paper">{q}</span>
|
||||||
|
{open === i ? <Minus size={16} className="flex-none text-mint" /> : <Plus size={16} className="flex-none text-paper/40" />}
|
||||||
|
</button>
|
||||||
|
{open === i && <p className="animate-fadeUp pb-6 text-sm leading-7 text-paper/60">{a}</p>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- CTA + Footer ---------- */
|
||||||
|
export function FooterCTA() {
|
||||||
|
return (
|
||||||
|
<footer className="border-t border-white/8">
|
||||||
|
<div className="mx-auto max-w-7xl px-5 py-28 text-center">
|
||||||
|
<p className="font-display text-6xl leading-[1.05] text-paper md:text-7xl">
|
||||||
|
Your goal is next.
|
||||||
|
</p>
|
||||||
|
<p className={`mx-auto mt-5 max-w-md ${mono} text-paper/45`}>
|
||||||
|
Join 100,000+ members who've made peace with their money
|
||||||
|
</p>
|
||||||
|
<Link href="/dashboard" className="mt-9 inline-flex items-center gap-2 rounded-full bg-mint px-8 py-4 text-sm font-bold text-ink transition hover:bg-paper">
|
||||||
|
Get started free <ArrowRight size={16} />
|
||||||
|
</Link>
|
||||||
|
<p className="mt-4 text-xs text-paper/35">No credit card required · Cancel anytime</p>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-white/8">
|
||||||
|
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-4 px-5 py-8">
|
||||||
|
<span className="flex items-center gap-2 font-display text-lg text-paper"><LeafMark size={20} /> Aartha</span>
|
||||||
|
<nav className={`flex flex-wrap gap-6 ${mono} text-paper/45`}>
|
||||||
|
<a href="#product" className="hover:text-paper">Product</a>
|
||||||
|
<a href="#pricing" className="hover:text-paper">Pricing</a>
|
||||||
|
<a href="#faq" className="hover:text-paper">FAQ</a>
|
||||||
|
<a href="#" className="hover:text-paper">Privacy</a>
|
||||||
|
<a href="#" className="hover:text-paper">Security</a>
|
||||||
|
</nav>
|
||||||
|
<span className="text-xs text-paper/35">© 2026 Aartha Technologies, Inc. · Your money. Your truth. Zero conflict.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
41
components/marketing/StoryBackdrop.tsx
Normal file
41
components/marketing/StoryBackdrop.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { GoalStory } from "@/lib/stories";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-bleed background video for a goal story, with a legibility scrim and a
|
||||||
|
* graceful gradient fallback (used while loading or if the video fails).
|
||||||
|
* Reusable: fills whatever positioned container it's placed in.
|
||||||
|
*/
|
||||||
|
export function StoryBackdrop({ story, dim = "strong" }: { story: GoalStory; dim?: "strong" | "soft" }) {
|
||||||
|
const [failed, setFailed] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 overflow-hidden" aria-hidden>
|
||||||
|
{/* gradient fallback always behind the video */}
|
||||||
|
<div className={`absolute inset-0 bg-gradient-to-br ${story.fallback}`} />
|
||||||
|
{!failed && (
|
||||||
|
<video
|
||||||
|
key={story.id}
|
||||||
|
className="absolute inset-0 h-full w-full scale-105 object-cover"
|
||||||
|
src={story.video}
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsInline
|
||||||
|
preload="auto"
|
||||||
|
onError={() => setFailed(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* scrim for text legibility */}
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
dim === "strong"
|
||||||
|
? "absolute inset-0 bg-gradient-to-r from-[#08130f]/85 via-[#08130f]/60 to-[#08130f]/30"
|
||||||
|
: "absolute inset-0 bg-gradient-to-t from-[#08130f]/80 via-[#08130f]/35 to-transparent"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,19 +3,13 @@ import { Card } from "@/components/ui/Card";
|
|||||||
import { Chip } from "@/components/ui/Chip";
|
import { Chip } from "@/components/ui/Chip";
|
||||||
import { ProgressBar } from "@/components/ui/ProgressBar";
|
import { ProgressBar } from "@/components/ui/ProgressBar";
|
||||||
import { PhoneFrame } from "./PhoneFrame";
|
import { PhoneFrame } from "./PhoneFrame";
|
||||||
|
import { StoryOnboarding } from "./StoryOnboarding";
|
||||||
|
|
||||||
export function MobileScreens() {
|
export function MobileScreens() {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-8 lg:grid-cols-3">
|
<div className="grid gap-8 lg:grid-cols-3">
|
||||||
<PhoneFrame title="Onboarding">
|
<PhoneFrame title="Onboarding — rotating goal story">
|
||||||
<div className="flex h-full flex-col justify-between">
|
<StoryOnboarding />
|
||||||
<div>
|
|
||||||
<Chip>Zero conflict finance</Chip>
|
|
||||||
<h2 className="mt-8 font-display text-5xl leading-none text-ink dark:text-paper">Meet your money clearly.</h2>
|
|
||||||
<p className="mt-4 text-sm leading-6 text-ink/65 dark:text-paper/65">Connect accounts, choose goals, and let Aartha build your first flex plan.</p>
|
|
||||||
</div>
|
|
||||||
<button className="mb-10 rounded-lg bg-forest py-3 font-bold text-white dark:bg-mint dark:text-ink">Get started</button>
|
|
||||||
</div>
|
|
||||||
</PhoneFrame>
|
</PhoneFrame>
|
||||||
<PhoneFrame title="Dashboard">
|
<PhoneFrame title="Dashboard">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
46
components/mobile/StoryOnboarding.tsx
Normal file
46
components/mobile/StoryOnboarding.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { RefreshCcw } from "lucide-react";
|
||||||
|
import { StoryBackdrop } from "@/components/marketing/StoryBackdrop";
|
||||||
|
import { randomStory, type GoalStory } from "@/lib/stories";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mobile onboarding screen with a rotating goal-story video background.
|
||||||
|
* A different human moment greets the user every time the app opens.
|
||||||
|
*/
|
||||||
|
export function StoryOnboarding() {
|
||||||
|
const [story, setStory] = useState<GoalStory | null>(null);
|
||||||
|
useEffect(() => setStory(randomStory()), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative -m-5 h-[calc(100%+40px)] overflow-hidden">
|
||||||
|
{story ? (
|
||||||
|
<StoryBackdrop story={story} dim="soft" />
|
||||||
|
) : (
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-[#0A5240] to-[#0F6E56]" />
|
||||||
|
)}
|
||||||
|
<div className="relative flex h-full flex-col justify-end p-5 pb-12">
|
||||||
|
{story && (
|
||||||
|
<>
|
||||||
|
<p className="text-[11px] font-bold uppercase tracking-wider text-mint">
|
||||||
|
{story.emoji} {story.tag}
|
||||||
|
</p>
|
||||||
|
<h2 className="mt-2 font-display text-3xl leading-tight text-paper">{story.headline}</h2>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-paper/75">{story.subline}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<p className="mt-4 text-xs font-semibold text-paper/60">
|
||||||
|
Your goal is next. Aartha gets you there.
|
||||||
|
</p>
|
||||||
|
<button className="mt-4 rounded-lg bg-mint py-3 font-bold text-ink">Get started</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setStory((s) => randomStory(s?.id))}
|
||||||
|
className="mt-3 inline-flex items-center justify-center gap-1.5 text-xs font-semibold text-paper/60"
|
||||||
|
>
|
||||||
|
<RefreshCcw size={12} /> See another story
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
components/ui/CountUp.tsx
Normal file
51
components/ui/CountUp.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
/** Animates from 0 to `value` when scrolled into view. */
|
||||||
|
export function CountUp({
|
||||||
|
value,
|
||||||
|
prefix = "",
|
||||||
|
suffix = "",
|
||||||
|
decimals = 0,
|
||||||
|
duration = 1400,
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
prefix?: string;
|
||||||
|
suffix?: string;
|
||||||
|
decimals?: number;
|
||||||
|
duration?: number;
|
||||||
|
}) {
|
||||||
|
const ref = useRef<HTMLSpanElement>(null);
|
||||||
|
const [display, setDisplay] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (!entries[0].isIntersecting) return;
|
||||||
|
io.disconnect();
|
||||||
|
const start = performance.now();
|
||||||
|
const tick = (now: number) => {
|
||||||
|
const p = Math.min((now - start) / duration, 1);
|
||||||
|
const eased = 1 - Math.pow(1 - p, 3); // ease-out cubic
|
||||||
|
setDisplay(value * eased);
|
||||||
|
if (p < 1) requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
requestAnimationFrame(tick);
|
||||||
|
},
|
||||||
|
{ threshold: 0.4 }
|
||||||
|
);
|
||||||
|
io.observe(el);
|
||||||
|
return () => io.disconnect();
|
||||||
|
}, [value, duration]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span ref={ref}>
|
||||||
|
{prefix}
|
||||||
|
{display.toLocaleString("en-US", { maximumFractionDigits: decimals, minimumFractionDigits: decimals })}
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,3 +1,21 @@
|
|||||||
|
/** Compact leaf mark for dark surfaces (marketing nav, footer). */
|
||||||
|
export function LeafMark({ size = 26 }: { size?: number }) {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" aria-hidden>
|
||||||
|
<path
|
||||||
|
d="M16 3C9 8 5.5 14 6.5 21.5 7.3 27 11.5 29.5 16 29.5c4.5 0 8.7-2.5 9.5-8C26.5 14 23 8 16 3Z"
|
||||||
|
fill="#1D9E75"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16 8v18M16 14l-4.5-3.5M16 18l5-4"
|
||||||
|
stroke="#F6F0E7"
|
||||||
|
strokeWidth="1.6"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Logo() {
|
export function Logo() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
98
lib/stories.ts
Normal file
98
lib/stories.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Goal stories — real human moments behind the numbers.
|
||||||
|
* One is picked at random every time the page opens (web hero + mobile onboarding).
|
||||||
|
* Videos are free-license stock from Pexels (pexels.com/license). For production,
|
||||||
|
* replace with licensed brand footage by swapping the `video` URLs (or move files
|
||||||
|
* into /public/videos and use local paths).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type GoalStory = {
|
||||||
|
id: string;
|
||||||
|
tag: string;
|
||||||
|
headline: string;
|
||||||
|
subline: string;
|
||||||
|
video: string;
|
||||||
|
emoji: string;
|
||||||
|
/** Tailwind gradient classes used if the video fails or while it loads */
|
||||||
|
fallback: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const goalStories: GoalStory[] = [
|
||||||
|
{
|
||||||
|
id: "retirement",
|
||||||
|
tag: "Goal achieved · Comfortable retirement",
|
||||||
|
headline: "After 34 years of showing up, Rita and Sam clocked out for good.",
|
||||||
|
subline: "Retired at 61 — two years early — with a plan that never wavered.",
|
||||||
|
video: "https://videos.pexels.com/video-files/7329851/7329851-sd_640_360_25fps.mp4",
|
||||||
|
emoji: "🌅",
|
||||||
|
fallback: "from-[#0A5240] via-[#0F6E56] to-[#B8771F]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "first-home",
|
||||||
|
tag: "Goal achieved · First home",
|
||||||
|
headline: "Maya got the keys to her first home on a Tuesday.",
|
||||||
|
subline: "18 months of flex budgeting. One very good Tuesday.",
|
||||||
|
video: "https://videos.pexels.com/video-files/4277738/4277738-sd_640_360_25fps.mp4",
|
||||||
|
emoji: "🔑",
|
||||||
|
fallback: "from-[#073D30] via-[#0F6E56] to-[#534AB7]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "rental-property",
|
||||||
|
tag: "Goal achieved · First rental property",
|
||||||
|
headline: "Dev closed on his first rental property this spring.",
|
||||||
|
subline: "Passive income started as one line item in his Aartha plan.",
|
||||||
|
video: "https://videos.pexels.com/video-files/4277475/4277475-sd_640_360_25fps.mp4",
|
||||||
|
emoji: "🏘️",
|
||||||
|
fallback: "from-[#0A5240] via-[#2E8B6A] to-[#1D9E75]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "debt-free",
|
||||||
|
tag: "Goal achieved · Debt-free day",
|
||||||
|
headline: "The day the Osei family paid off the last of $42,000.",
|
||||||
|
subline: "Every extra dollar had a job. This was the payoff.",
|
||||||
|
video: "https://videos.pexels.com/video-files/7712328/7712328-sd_640_360_30fps.mp4",
|
||||||
|
emoji: "🎉",
|
||||||
|
fallback: "from-[#3E3792] via-[#534AB7] to-[#0F6E56]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "college-fund",
|
||||||
|
tag: "Goal achieved · College fund",
|
||||||
|
headline: "Amara walked the stage completely debt-free.",
|
||||||
|
subline: "Her parents started the fund 12 years ago — $180 a month.",
|
||||||
|
video: "https://videos.pexels.com/video-files/7712350/7712350-sd_640_360_30fps.mp4",
|
||||||
|
emoji: "🎓",
|
||||||
|
fallback: "from-[#073D30] via-[#0F6E56] to-[#9BD9C5]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "new-baby",
|
||||||
|
tag: "Goal achieved · Ready for baby",
|
||||||
|
headline: "Leo arrived. The emergency fund was already there.",
|
||||||
|
subline: "Nine months to prepare — financially calm on day one.",
|
||||||
|
video: "https://videos.pexels.com/video-files/7918438/7918438-sd_640_360_25fps.mp4",
|
||||||
|
emoji: "👶",
|
||||||
|
fallback: "from-[#0F6E56] via-[#2E8B6A] to-[#E8A23A]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "small-business",
|
||||||
|
tag: "Goal achieved · Own boss",
|
||||||
|
headline: "Priya opened her studio's doors with zero business debt.",
|
||||||
|
subline: "She saved her way to independence, one month at a time.",
|
||||||
|
video: "https://videos.pexels.com/video-files/6685171/6685171-sd_640_360_30fps.mp4",
|
||||||
|
emoji: "🚀",
|
||||||
|
fallback: "from-[#96601A] via-[#B8771F] to-[#0F6E56]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dream-trip",
|
||||||
|
tag: "Goal achieved · Dream trip",
|
||||||
|
headline: "Three generations, one beach, fully paid for.",
|
||||||
|
subline: "The Reyes family trip fund hit 100% in June.",
|
||||||
|
video: "https://videos.pexels.com/video-files/7850307/7850307-sd_640_360_30fps.mp4",
|
||||||
|
emoji: "🏖️",
|
||||||
|
fallback: "from-[#0A5240] via-[#1D9E75] to-[#534AB7]",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function randomStory(excludeId?: string): GoalStory {
|
||||||
|
const pool = goalStories.filter((s) => s.id !== excludeId);
|
||||||
|
return pool[Math.floor(Math.random() * pool.length)];
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user