From Mohan Initial Push

This commit is contained in:
MOHAN 2026-07-08 16:29:41 +05:30
parent 200f442381
commit f4b5e4bc74
39 changed files with 7380 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
# Dependencies
/node_modules/
# Next.js build and static export output
/.next/
/out/
# Local environment files
.env*
!.env.example
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# TypeScript
*.tsbuildinfo
next-env.d.ts
# OS and editor files
.DS_Store
Thumbs.db
.vscode/
.idea/

31
app/app/page.tsx Normal file
View File

@ -0,0 +1,31 @@
"use client";
import { useEffect, useState } from "react";
import { AskAartha } from "@/components/dashboard/AskAartha";
import { Sidebar, type ViewKey } from "@/components/dashboard/Sidebar";
import { Topbar } from "@/components/dashboard/Topbar";
import { DashboardView } from "@/components/dashboard/views";
export default function AppPage() {
const [active, setActive] = useState<ViewKey>("overview");
const [dark, setDark] = useState(false);
useEffect(() => {
document.documentElement.classList.toggle("dark", dark);
}, [dark]);
return (
<main className="min-h-screen bg-cream dark:bg-[#08130f]">
<div className="flex">
<Sidebar active={active} onChange={setActive} />
<section className="min-w-0 flex-1">
<Topbar dark={dark} onToggleDark={() => setDark((value) => !value)} />
<div className="p-5 lg:p-8">
<DashboardView active={active} />
</div>
</section>
</div>
<AskAartha />
</main>
);
}

3
app/dashboard/page.tsx Normal file
View File

@ -0,0 +1,3 @@
"use client";
export { default } from "../app/page";

49
app/globals.css Normal file
View File

@ -0,0 +1,49 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
color-scheme: light;
}
.dark {
color-scheme: dark;
}
html {
scroll-behavior: smooth;
}
body {
background: #f6f0e7;
color: #10231d;
}
.dark body {
background: #08130f;
color: #fffdf8;
}
::selection {
background: rgba(29, 158, 117, 0.24);
}
.grain {
background-image:
radial-gradient(circle at 20% 20%, rgba(29, 158, 117, 0.14), transparent 32%),
radial-gradient(circle at 80% 0%, rgba(127, 119, 221, 0.16), transparent 26%),
linear-gradient(135deg, rgba(255,255,255,.78), rgba(255,255,255,.42));
}
.dark .grain {
background-image:
radial-gradient(circle at 18% 12%, rgba(29, 158, 117, 0.2), transparent 32%),
radial-gradient(circle at 85% 8%, rgba(127, 119, 221, 0.2), transparent 28%),
linear-gradient(135deg, rgba(16,35,29,.95), rgba(8,19,15,.96));
}
html {
scroll-behavior: smooth;
}

29
app/layout.tsx Normal file
View File

@ -0,0 +1,29 @@
import type { Metadata } from "next";
import { Inter, DM_Serif_Display } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
const display = DM_Serif_Display({
subsets: ["latin"],
weight: "400",
style: ["normal", "italic"],
variable: "--font-display",
});
export const metadata: Metadata = {
title: "Aartha — Your money. Your truth. Zero conflict.",
description:
"Subscription-only personal finance. Zero ads. Zero referral fees. Every insight aligned only with you.",
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${inter.variable} ${display.variable} bg-cream font-sans text-ink antialiased`}>
{children}
</body>
</html>
);
}

23
app/mobile/page.tsx Normal file
View File

@ -0,0 +1,23 @@
import { MobileScreens } from "@/components/mobile/MobileScreens";
import { ButtonLink } from "@/components/ui/Button";
import { Logo } from "@/components/ui/Logo";
export default function MobilePage() {
return (
<main className="min-h-screen bg-cream px-5 py-8 dark:bg-[#08130f]">
<div className="mx-auto max-w-7xl">
<div className="flex items-center justify-between">
<Logo />
<ButtonLink href="/" variant="secondary">Back home</ButtonLink>
</div>
<div className="mt-12">
<h1 className="font-display text-5xl text-ink dark:text-paper">Mobile product screens</h1>
<p className="mt-3 max-w-2xl text-ink/65 dark:text-paper/65">Reusable phone frames showing onboarding, flex dashboard, and credit coaching surfaces.</p>
</div>
<div className="mt-10">
<MobileScreens />
</div>
</div>
</main>
);
}

16
app/page.tsx Normal file
View File

@ -0,0 +1,16 @@
import { Hero } from "@/components/marketing/Hero";
import { Navbar } from "@/components/marketing/Navbar";
import { CTA, FeatureGrid, PersonaGrid, Pricing } from "@/components/marketing/Sections";
export default function HomePage() {
return (
<main>
<Navbar />
<Hero />
<FeatureGrid />
<PersonaGrid />
<Pricing />
<CTA />
</main>
);
}

View File

@ -0,0 +1,38 @@
export function AreaChart({ values, color = "#0F6E56" }: { values: number[]; color?: string }) {
const max = Math.max(...values);
const min = Math.min(...values);
const points = values.map((v, i) => {
const x = (i / (values.length - 1)) * 100;
const y = 70 - ((v - min) / (max - min || 1)) * 50;
return `${x},${y}`;
});
const area = `0,78 ${points.join(" ")} 100,78`;
const gradientId = `areaFill-${color.replace(/[^a-zA-Z0-9]/g, "")}`;
return (
<svg
viewBox="0 0 100 80"
preserveAspectRatio="none"
className="h-40 w-full"
role="img"
aria-label="Trend chart"
>
<defs>
<linearGradient id={gradientId} x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity=".28" />
<stop offset="100%" stopColor={color} stopOpacity="0" />
</linearGradient>
</defs>
<polygon points={area} fill={`url(#${gradientId})`} />
<polyline
points={points.join(" ")}
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
vectorEffect="non-scaling-stroke"
/>
</svg>
);
}

View File

@ -0,0 +1,25 @@
export function DonutChart({ segments }: { segments: { label: string; value: number; color: string }[] }) {
let offset = 25;
const total = segments.reduce((sum, segment) => sum + segment.value, 0);
return (
<div className="flex items-center gap-5">
<svg viewBox="0 0 42 42" className="size-28 -rotate-90">
<circle cx="21" cy="21" r="15.9" fill="none" stroke="currentColor" strokeWidth="6" className="text-forest/10 dark:text-white/10" />
{segments.map((segment) => {
const dash = (segment.value / total) * 100;
const circle = <circle key={segment.label} cx="21" cy="21" r="15.9" fill="none" stroke={segment.color} strokeDasharray={`${dash} ${100 - dash}`} strokeDashoffset={offset} strokeWidth="6" />;
offset -= dash;
return circle;
})}
</svg>
<div className="space-y-2">
{segments.map((segment) => (
<div key={segment.label} className="flex items-center gap-2 text-sm text-ink/70 dark:text-paper/70">
<span className="size-2.5 rounded-full" style={{ background: segment.color }} />
{segment.label} · {segment.value}%
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,14 @@
export function GaugeChart({ value, label }: { value: number; label: string }) {
const rotation = Math.min(180, Math.max(0, value * 1.8));
return (
<div className="text-center">
<div className="relative mx-auto h-32 w-64 overflow-hidden">
<div className="absolute inset-x-0 top-0 h-64 rounded-full border-[24px] border-forest/10 dark:border-white/10" />
<div className="absolute inset-x-0 top-0 h-64 rounded-full border-[24px] border-transparent border-l-mint border-t-mint" style={{ transform: `rotate(${rotation - 135}deg)` }} />
<div className="absolute bottom-0 left-1/2 h-1 w-24 origin-left rounded-full bg-ink dark:bg-paper" style={{ transform: `rotate(${rotation - 180}deg)` }} />
</div>
<p className="font-display text-4xl text-forest dark:text-mint">{value}%</p>
<p className="text-sm font-semibold text-ink/60 dark:text-paper/60">{label}</p>
</div>
);
}

View File

@ -0,0 +1,10 @@
export function MiniBars({ values }: { values: number[] }) {
const max = Math.max(...values);
return (
<div className="flex h-24 items-end gap-2">
{values.map((value, index) => (
<div key={`${value}-${index}`} className="flex-1 rounded-t bg-mint/75" style={{ height: `${(value / max) * 100}%` }} />
))}
</div>
);
}

View File

@ -0,0 +1,34 @@
"use client";
import { useState } from "react";
import { Bot, Send } from "lucide-react";
import { Button } from "@/components/ui/Button";
const answers: Record<string, string> = {
"Can I spend $200 this weekend?": "Yes, but keep it under $154 if you want to stay on pace for your emergency-fund goal.",
"Why did my score move?": "Utilization dropped from 34% to 27%, which likely helped your FICO 8 estimate. Keep cards below 30% before statement close.",
"Which card for groceries?": "Use Sapphire Preferred for groceries this week. It beats your flat 2% card based on current bonus categories."
};
export function AskAartha() {
const [open, setOpen] = useState(false);
const [message, setMessage] = useState("Can I spend $200 this weekend?");
return (
<div className="fixed bottom-5 right-5 z-50">
{open ? (
<div className="mb-3 w-[min(380px,calc(100vw-40px))] rounded-lg border border-forest/10 bg-paper p-4 shadow-soft dark:border-white/10 dark:bg-[#10231d]">
<div className="flex items-center gap-2 font-bold text-ink dark:text-paper"><Bot size={18} /> Ask Aartha</div>
<div className="mt-4 rounded-lg bg-forest/8 p-3 text-sm leading-6 text-ink/70 dark:bg-white/10 dark:text-paper/70">{answers[message]}</div>
<div className="mt-3 flex flex-wrap gap-2">
{Object.keys(answers).map((prompt) => (
<button key={prompt} onClick={() => setMessage(prompt)} className="rounded-full bg-white px-3 py-1 text-xs font-semibold text-forest ring-1 ring-forest/10 dark:bg-white/10 dark:text-mint dark:ring-white/10">{prompt}</button>
))}
</div>
<p className="mt-3 text-xs text-ink/45 dark:text-paper/45">Educational guidance only, not regulated financial advice.</p>
</div>
) : null}
<Button onClick={() => setOpen((value) => !value)}><Send size={17} /> Ask Aartha</Button>
</div>
);
}

View File

@ -0,0 +1,42 @@
"use client";
import { BarChart3, Bot, CreditCard, Flag, Home, PieChart, ReceiptText, WalletCards } from "lucide-react";
import { Logo } from "@/components/ui/Logo";
export const navItems = [
["overview", "Overview", Home],
["transactions", "Transactions", ReceiptText],
["budget", "Budget", PieChart],
["goals", "Goals", Flag],
["networth", "Net Worth", BarChart3],
["credit", "Credit & Rewards", CreditCard],
["insights", "AI Insights", Bot]
] as const;
export type ViewKey = (typeof navItems)[number][0];
export function Sidebar({ active, onChange }: { active: ViewKey; onChange: (view: ViewKey) => void }) {
return (
<aside className="hidden min-h-screen w-72 border-r border-forest/10 bg-paper/80 p-5 dark:border-white/10 dark:bg-white/[0.04] lg:block">
<Logo />
<nav className="mt-8 space-y-2">
{navItems.map(([key, label, Icon]) => (
<button
key={key}
onClick={() => onChange(key)}
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-bold transition ${
active === key ? "bg-forest text-white dark:bg-mint dark:text-ink" : "text-ink/65 hover:bg-forest/8 dark:text-paper/65 dark:hover:bg-white/10"
}`}
>
<Icon size={18} />
{label}
</button>
))}
</nav>
<div className="mt-8 rounded-lg bg-forest/8 p-4 text-sm leading-6 text-ink/65 dark:bg-white/[0.08] dark:text-paper/65">
<WalletCards className="mb-3 text-forest dark:text-mint" size={20} />
Plaid sync healthy across 8 of 8 institutions.
</div>
</aside>
);
}

View File

@ -0,0 +1,21 @@
"use client";
import { Moon, Search, Sun } from "lucide-react";
import { Button } from "@/components/ui/Button";
export function Topbar({ dark, onToggleDark }: { dark: boolean; onToggleDark: () => void }) {
return (
<header className="flex items-center justify-between border-b border-forest/10 bg-cream/70 px-5 py-4 backdrop-blur dark:border-white/10 dark:bg-[#08130f]/80">
<div>
<p className="text-sm font-bold uppercase tracking-wide text-forest dark:text-mint">Aartha web app</p>
<h1 className="font-display text-3xl text-ink dark:text-paper">Money command center</h1>
</div>
<div className="flex items-center gap-3">
<div className="hidden items-center gap-2 rounded-lg bg-white/80 px-3 py-2 text-sm text-ink/50 ring-1 ring-forest/10 dark:bg-white/10 dark:text-paper/50 dark:ring-white/10 md:flex">
<Search size={16} /> Search accounts, transactions, budgets
</div>
<Button variant="secondary" onClick={onToggleDark} aria-label="Toggle dark mode">{dark ? <Sun size={17} /> : <Moon size={17} />}</Button>
</div>
</header>
);
}

View File

@ -0,0 +1,138 @@
"use client";
import { AreaChart } from "@/components/charts/AreaChart";
import { DonutChart } from "@/components/charts/DonutChart";
import { GaugeChart } from "@/components/charts/GaugeChart";
import { MiniBars } from "@/components/charts/MiniBars";
import { FactorRow } from "@/components/finance/FactorRow";
import { GoalCard } from "@/components/finance/GoalCard";
import { InsightCard } from "@/components/finance/InsightCard";
import { TransactionRow } from "@/components/finance/TransactionRow";
import { Card, StatCard } from "@/components/ui/Card";
import { Chip } from "@/components/ui/Chip";
import { ProgressBar } from "@/components/ui/ProgressBar";
import { goals, transactions } from "@/lib/data";
import type { ViewKey } from "./Sidebar";
export function DashboardView({ active }: { active: ViewKey }) {
const views: Record<ViewKey, JSX.Element> = {
overview: <Overview />,
transactions: <Transactions />,
budget: <Budget />,
goals: <Goals />,
networth: <NetWorth />,
credit: <CreditRewards />,
insights: <Insights />
};
return views[active];
}
function Overview() {
return (
<div className="space-y-5">
<div className="grid gap-4 md:grid-cols-4">
<StatCard label="Net worth" value="$184,220" detail="+$2,840 this month" />
<StatCard label="Spent today" value="$93" detail="$61 below pace" tone="saffron" />
<StatCard label="Flex left" value="$1,184" detail="17 days remaining" tone="indigo" />
<StatCard label="FICO 8" value="742" detail="+21 in 90 days" tone="forest" />
</div>
<div className="grid gap-5 lg:grid-cols-[1.5fr_.9fr]">
<Card>
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-ink dark:text-paper">Net worth trend</h2>
<Chip>12 months</Chip>
</div>
<AreaChart values={[112, 116, 121, 129, 133, 141, 148, 153, 161, 169, 177, 184]} />
</Card>
<Card>
<h2 className="text-xl font-bold text-ink dark:text-paper">Spending mix</h2>
<div className="mt-6">
<DonutChart segments={[{ label: "Needs", value: 48, color: "#0F6E56" }, { label: "Wants", value: 32, color: "#E8A23A" }, { label: "Goals", value: 20, color: "#7F77DD" }]} />
</div>
</Card>
</div>
<div className="grid gap-5 lg:grid-cols-2">
<InsightCard title="Cash-flow cushion is improving" tag="AI coach" body="Your next 30 days look $420 stronger than last month because subscription spend dropped and payroll timing is favorable." />
<InsightCard title="Rewards opportunity found" tag="Cards" body="Move grocery purchases to Sapphire Preferred through July 15 to earn an estimated $18 more in rewards." />
</div>
</div>
);
}
function Transactions() {
return (
<Card>
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-ink dark:text-paper">Recent transactions</h2>
<Chip tone="indigo">Undo-ready categorization</Chip>
</div>
<div className="mt-5 space-y-3">
{transactions.map(([merchant, category, amount, date]) => <TransactionRow key={merchant} merchant={merchant} category={category} amount={amount} date={date} />)}
</div>
</Card>
);
}
function Budget() {
const categories = [["Housing", 62], ["Groceries", 84], ["Dining", 109], ["Transport", 42], ["Subscriptions", 77]] as const;
return (
<div className="grid gap-5 lg:grid-cols-[.9fr_1.1fr]">
<Card>
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-ink dark:text-paper">Flex budget</h2>
<Chip>Default mode</Chip>
</div>
<div className="mt-8"><GaugeChart value={72} label="$93 safe to spend today" /></div>
</Card>
<Card>
<h2 className="text-xl font-bold text-ink dark:text-paper">Category guardrails</h2>
<div className="mt-5 space-y-5">
{categories.map(([name, value]) => <ProgressBar key={name} label={name} value={value} />)}
</div>
</Card>
</div>
);
}
function Goals() {
return <div className="grid gap-5 md:grid-cols-3">{goals.map((goal) => <GoalCard key={goal.name} {...goal} />)}</div>;
}
function NetWorth() {
return (
<div className="grid gap-5 lg:grid-cols-[1.2fr_.8fr]">
<Card><h2 className="text-xl font-bold text-ink dark:text-paper">Assets over time</h2><AreaChart values={[122, 128, 136, 141, 152, 167, 184]} color="#7F77DD" /></Card>
<Card><h2 className="text-xl font-bold text-ink dark:text-paper">Allocation</h2><div className="mt-6"><DonutChart segments={[{ label: "Cash", value: 18, color: "#1D9E75" }, { label: "Investments", value: 64, color: "#7F77DD" }, { label: "Retirement", value: 18, color: "#E8A23A" }]} /></div></Card>
</div>
);
}
function CreditRewards() {
return (
<div className="grid gap-5 lg:grid-cols-2">
<Card>
<h2 className="text-xl font-bold text-ink dark:text-paper">FICO 8 factors</h2>
<div className="mt-4">
<FactorRow label="Utilization" value="27%" detail="Below 30%, improving" />
<FactorRow label="Payment history" value="100%" detail="No missed payments" />
<FactorRow label="Credit age" value="6.2y" detail="Stable contributor" />
</div>
</Card>
<Card>
<h2 className="text-xl font-bold text-ink dark:text-paper">Rewards this month</h2>
<div className="mt-6"><MiniBars values={[12, 18, 9, 24, 31, 22, 38]} /></div>
<p className="mt-4 text-sm text-ink/62 dark:text-paper/62">Estimated $142 earned; $28 more available by shifting grocery spend.</p>
</Card>
</div>
);
}
function Insights() {
return (
<div className="grid gap-5 md:grid-cols-3">
<InsightCard tag="Budget" title="Dining is trending hot" body="You are 9% over the recommended pacing curve. Aartha can auto-adjust flex spend for the next 10 days." />
<InsightCard tag="Subscription" title="Duplicate media spend" body="Two music services posted this month. Canceling one protects $132 annually." />
<InsightCard tag="Goals" title="Emergency fund can arrive early" body="Rolling over unused flex could pull the target date from October to September." />
</div>
);
}

View File

@ -0,0 +1,11 @@
export function FactorRow({ label, value, detail }: { label: string; value: string; detail: string }) {
return (
<div className="flex items-center justify-between border-b border-forest/10 py-3 last:border-0 dark:border-white/10">
<div>
<p className="font-semibold text-ink dark:text-paper">{label}</p>
<p className="text-sm text-ink/55 dark:text-paper/55">{detail}</p>
</div>
<p className="font-bold text-forest dark:text-mint">{value}</p>
</div>
);
}

View File

@ -0,0 +1,21 @@
import { Card } from "@/components/ui/Card";
import { ProgressBar } from "@/components/ui/ProgressBar";
export function GoalCard({ name, saved, target, date }: { name: string; saved: number; target: number; date: string }) {
const pct = Math.round((saved / target) * 100);
return (
<Card className="p-4">
<div className="flex items-start justify-between">
<div>
<h3 className="font-bold text-ink dark:text-paper">{name}</h3>
<p className="text-sm text-ink/55 dark:text-paper/55">Projected {date}</p>
</div>
<p className="font-display text-2xl text-forest dark:text-mint">{pct}%</p>
</div>
<div className="mt-5">
<ProgressBar value={pct} />
</div>
<p className="mt-3 text-sm text-ink/60 dark:text-paper/60">${saved.toLocaleString()} of ${target.toLocaleString()}</p>
</Card>
);
}

View File

@ -0,0 +1,12 @@
import { Card } from "@/components/ui/Card";
import { Chip } from "@/components/ui/Chip";
export function InsightCard({ title, body, tag }: { title: string; body: string; tag: string }) {
return (
<Card className="p-4">
<Chip tone="indigo">{tag}</Chip>
<h3 className="mt-4 text-lg font-bold text-ink dark:text-paper">{title}</h3>
<p className="mt-2 text-sm leading-6 text-ink/65 dark:text-paper/65">{body}</p>
</Card>
);
}

View File

@ -0,0 +1,16 @@
import { Chip } from "@/components/ui/Chip";
export function TransactionRow({ merchant, category, amount, date }: { merchant: string; category: string; amount: string; date: string }) {
return (
<div className="flex items-center justify-between rounded-lg border border-forest/8 bg-white/60 p-3 dark:border-white/10 dark:bg-white/[0.04]">
<div>
<p className="font-semibold text-ink dark:text-paper">{merchant}</p>
<p className="text-sm text-ink/55 dark:text-paper/55">{date}</p>
</div>
<div className="text-right">
<p className="font-bold text-ink dark:text-paper">{amount}</p>
<Chip>{category}</Chip>
</div>
</div>
);
}

View File

@ -0,0 +1,48 @@
import { ArrowRight, ShieldCheck, Sparkles } from "lucide-react";
import { AreaChart } from "@/components/charts/AreaChart";
import { ButtonLink } from "@/components/ui/Button";
import { Card, StatCard } from "@/components/ui/Card";
import { Chip } from "@/components/ui/Chip";
export function Hero() {
return (
<section className="grain">
<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]">
<div>
<Chip>Subscription-only · zero ads · zero referral bias</Chip>
<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.
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-ink/68 dark:text-paper/68">
Aartha unifies budgets, cash flow, credit, rewards, goals, and AI coaching into one calm finance cockpit built around the user&apos;s wellbeing.
</p>
<div className="mt-8 flex flex-wrap gap-3">
<ButtonLink href="/dashboard">View dashboard <ArrowRight size={17} /></ButtonLink>
<ButtonLink href="/mobile" variant="secondary">Mobile screens</ButtonLink>
</div>
<div className="mt-8 flex flex-wrap gap-5 text-sm font-semibold text-ink/60 dark:text-paper/60">
<span className="inline-flex items-center gap-2"><ShieldCheck size={16} /> Bank-grade posture</span>
<span className="inline-flex items-center gap-2"><Sparkles size={16} /> Ask Aartha AI coach</span>
</div>
</div>
<Card className="p-5">
<div className="grid gap-4 sm:grid-cols-3">
<StatCard label="Net worth" value="$184k" detail="+8.2% YTD" />
<StatCard label="Flex left" value="$93" detail="Safe to spend today" tone="saffron" />
<StatCard label="FICO 8" value="742" detail="+21 since April" tone="indigo" />
</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>
<p className="text-sm font-bold uppercase tracking-wide text-ink/45 dark:text-paper/45">Cash-flow forecast</p>
<p className="font-display text-4xl text-forest dark:text-mint">$7,420</p>
</div>
<Chip tone="saffron">Next 30 days</Chip>
</div>
<AreaChart values={[8, 12, 11, 19, 18, 24, 31, 28, 36]} />
</div>
</Card>
</div>
</section>
);
}

View File

@ -0,0 +1,19 @@
import { ButtonLink } from "@/components/ui/Button";
import { Logo } from "@/components/ui/Logo";
export function Navbar() {
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">
<div className="mx-auto flex max-w-7xl items-center justify-between px-5 py-4">
<Logo />
<nav className="hidden items-center gap-7 text-sm font-semibold text-ink/70 dark:text-paper/70 md:flex">
<a href="#features">Features</a>
<a href="#pricing">Pricing</a>
<a href="/dashboard">Dashboard</a>
<a href="/mobile">Mobile</a>
</nav>
<ButtonLink href="/dashboard" size="sm">Open app</ButtonLink>
</div>
</header>
);
}

View File

@ -0,0 +1,78 @@
import { personas, prices } from "@/lib/data";
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 = [
["Flex budgeting", "Speedometer-style daily allowance after fixed expenses and goals."],
["Credit clarity", "Real FICO 8 factors translated into actions users can actually take."],
["Rewards intelligence", "Recommend the best card for each purchase based on live transactions."],
["Shared finances", "Partner and roommate views with privacy-aware collaboration."]
];
export function FeatureGrid() {
return (
<section id="features" className="mx-auto max-w-7xl px-5 py-24">
<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." />
<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() {
return (
<section className="bg-paper/70 py-24 dark:bg-white/[0.03]">
<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." />
<div className="mt-12 grid gap-4 md:grid-cols-5">
{personas.map(([name, body], index) => (
<Card key={name} className="p-4">
<Chip tone={index % 2 ? "indigo" : "mint"}>{String(index + 1).padStart(2, "0")}</Chip>
<h3 className="mt-4 font-bold text-ink dark:text-paper">{name}</h3>
<p className="mt-3 text-sm leading-6 text-ink/62 dark:text-paper/62">{body}</p>
</Card>
))}
</div>
</div>
</section>
);
}
export function Pricing() {
return (
<section id="pricing" className="mx-auto max-w-7xl px-5 py-24">
<SectionHeading eyebrow="Pricing" title="Clear subscription tiers. No hidden incentives." body="Pricing follows the PRD and reinforces Aartha&apos;s zero-conflict positioning." />
<div className="mt-12 grid gap-4 md:grid-cols-4">
{prices.map(([name, price, body]) => (
<Card key={name} className={name === "Plus" ? "ring-2 ring-mint" : ""}>
{name === "Plus" ? <Chip tone="saffron">Most popular</Chip> : null}
<h3 className="mt-4 text-xl font-bold text-ink dark:text-paper">{name}</h3>
<p className="mt-2 font-display text-4xl text-forest dark:text-mint">{price}</p>
<p className="mt-3 min-h-12 text-sm leading-6 text-ink/62 dark:text-paper/62">{body}</p>
<ButtonLink href="/dashboard" variant={name === "Plus" ? "primary" : "secondary"} className="mt-6 w-full">Preview tier</ButtonLink>
</Card>
))}
</div>
</section>
);
}
export function CTA() {
return (
<section className="px-5 pb-24">
<div className="mx-auto max-w-7xl rounded-lg bg-forest p-10 text-paper dark:bg-mint dark:text-ink md:p-14">
<h2 className="font-display text-5xl">Ready for a component-based handoff.</h2>
<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>
<ButtonLink href="/dashboard" variant="secondary" className="mt-8">Explore app prototype</ButtonLink>
</div>
</section>
);
}

View File

@ -0,0 +1,45 @@
import { GaugeChart } from "@/components/charts/GaugeChart";
import { Card } from "@/components/ui/Card";
import { Chip } from "@/components/ui/Chip";
import { ProgressBar } from "@/components/ui/ProgressBar";
import { PhoneFrame } from "./PhoneFrame";
export function MobileScreens() {
return (
<div className="grid gap-8 lg:grid-cols-3">
<PhoneFrame title="Onboarding">
<div className="flex h-full flex-col justify-between">
<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 title="Dashboard">
<div>
<p className="text-sm text-ink/50 dark:text-paper/50">Good morning</p>
<h2 className="font-display text-4xl text-forest dark:text-mint">$93</h2>
<p className="text-sm font-semibold text-ink/60 dark:text-paper/60">safe to spend today</p>
<Card className="mt-5 p-4"><GaugeChart value={72} label="Flex pace" /></Card>
<div className="mt-4 space-y-3">
<ProgressBar label="Groceries" value={84} />
<ProgressBar label="Dining" value={109} />
</div>
</div>
</PhoneFrame>
<PhoneFrame title="Credit & AI">
<div>
<Chip tone="indigo">FICO 8</Chip>
<h2 className="mt-4 font-display text-6xl text-forest dark:text-mint">742</h2>
<p className="text-sm text-ink/60 dark:text-paper/60">+21 since April</p>
<Card className="mt-6 p-4">
<h3 className="font-bold text-ink dark:text-paper">Ask Aartha</h3>
<p className="mt-3 text-sm leading-6 text-ink/65 dark:text-paper/65">Utilization improved. Keep cards below 30% before statement close to protect the gain.</p>
</Card>
</div>
</PhoneFrame>
</div>
);
}

View File

@ -0,0 +1,13 @@
import type { ReactNode } from "react";
export function PhoneFrame({ title, children }: { title: string; children: ReactNode }) {
return (
<div>
<p className="mb-3 text-center text-sm font-bold text-ink/60 dark:text-paper/60">{title}</p>
<div className="mx-auto h-[620px] w-[310px] overflow-hidden rounded-[34px] border-[10px] border-ink bg-paper shadow-soft dark:border-black dark:bg-[#10231d]">
<div className="mx-auto mt-3 h-6 w-28 rounded-full bg-ink/90 dark:bg-black" />
<div className="h-full p-5">{children}</div>
</div>
</div>
);
}

37
components/ui/Button.tsx Normal file
View File

@ -0,0 +1,37 @@
import Link from "next/link";
import { clsx } from "clsx";
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from "react";
type Common = {
children: ReactNode;
variant?: "primary" | "secondary" | "ghost";
size?: "sm" | "md";
className?: string;
};
const variants = {
primary: "bg-forest text-white shadow-lift hover:bg-mint dark:bg-mint dark:text-ink",
secondary: "bg-white/85 text-ink ring-1 ring-forest/15 hover:bg-white dark:bg-white/10 dark:text-paper dark:ring-white/15",
ghost: "text-forest hover:bg-forest/8 dark:text-mint dark:hover:bg-white/10"
};
const sizes = {
sm: "h-9 px-3 text-sm",
md: "h-11 px-5 text-sm"
};
export function Button({ children, variant = "primary", size = "md", className, ...props }: Common & ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button className={clsx("inline-flex items-center justify-center gap-2 rounded-lg font-semibold transition", variants[variant], sizes[size], className)} {...props}>
{children}
</button>
);
}
export function ButtonLink({ children, variant = "primary", size = "md", className, href, ...props }: Common & AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) {
return (
<Link href={href} className={clsx("inline-flex items-center justify-center gap-2 rounded-lg font-semibold transition", variants[variant], sizes[size], className)} {...props}>
{children}
</Link>
);
}

27
components/ui/Card.tsx Normal file
View File

@ -0,0 +1,27 @@
import { clsx } from "clsx";
import type { HTMLAttributes, ReactNode } from "react";
export function Card({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
return (
<div className={clsx("rounded-lg border border-forest/10 bg-paper/88 p-5 shadow-soft backdrop-blur dark:border-white/10 dark:bg-white/[0.06]", className)} {...props}>
{children}
</div>
);
}
export function StatCard({ label, value, detail, tone = "forest" }: { label: string; value: string; detail: string; tone?: "forest" | "indigo" | "saffron" | "coral" }) {
const color = {
forest: "text-forest dark:text-mint",
indigo: "text-indigo",
saffron: "text-saffron",
coral: "text-coral"
}[tone];
return (
<Card className="p-4">
<p className="text-xs font-semibold uppercase tracking-wide text-ink/50 dark:text-paper/50">{label}</p>
<p className={`mt-2 font-display text-3xl ${color}`}>{value}</p>
<p className="mt-1 text-sm text-ink/60 dark:text-paper/60">{detail}</p>
</Card>
);
}

12
components/ui/Chip.tsx Normal file
View File

@ -0,0 +1,12 @@
import { clsx } from "clsx";
import type { ReactNode } from "react";
export function Chip({ children, tone = "mint", className }: { children: ReactNode; tone?: "mint" | "indigo" | "saffron" | "coral"; className?: string }) {
const tones = {
mint: "bg-mint/12 text-forest ring-mint/20 dark:text-mint",
indigo: "bg-indigo/12 text-indigo ring-indigo/20",
saffron: "bg-saffron/14 text-[#8a5a12] ring-saffron/25 dark:text-saffron",
coral: "bg-coral/12 text-coral ring-coral/20"
};
return <span className={clsx("inline-flex items-center rounded-full px-3 py-1 text-xs font-bold ring-1", tones[tone], className)}>{children}</span>;
}

11
components/ui/Delta.tsx Normal file
View File

@ -0,0 +1,11 @@
import { TrendingDown, TrendingUp } from "lucide-react";
export function Delta({ value, good = true }: { value: string; good?: boolean }) {
const Icon = good ? TrendingUp : TrendingDown;
return (
<span className={`inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-bold ${good ? "bg-mint/12 text-forest dark:text-mint" : "bg-coral/12 text-coral"}`}>
<Icon size={13} />
{value}
</span>
);
}

11
components/ui/Logo.tsx Normal file
View File

@ -0,0 +1,11 @@
export function Logo() {
return (
<div className="flex items-center gap-3">
<div className="grid size-10 place-items-center rounded-lg bg-forest text-lg font-bold text-white shadow-lift dark:bg-mint dark:text-ink">A</div>
<div>
<p className="font-display text-2xl leading-5 text-ink dark:text-paper">Aartha</p>
<p className="text-[11px] font-bold uppercase tracking-[0.18em] text-forest/70 dark:text-mint/80">Labs</p>
</div>
</div>
);
}

View File

@ -0,0 +1,16 @@
export function ProgressBar({ value, label }: { value: number; label?: string }) {
const tone = value > 100 ? "bg-coral" : value >= 80 ? "bg-saffron" : "bg-mint";
return (
<div>
{label ? (
<div className="mb-2 flex justify-between text-sm text-ink/65 dark:text-paper/65">
<span>{label}</span>
<span>{value}%</span>
</div>
) : null}
<div className="h-2.5 overflow-hidden rounded-full bg-forest/10 dark:bg-white/10">
<div className={`${tone} h-full rounded-full transition-all`} style={{ width: `${Math.min(value, 115)}%` }} />
</div>
</div>
);
}

5
components/ui/Reveal.tsx Normal file
View File

@ -0,0 +1,5 @@
import type { ReactNode } from "react";
export function Reveal({ children, className = "" }: { children: ReactNode; className?: string }) {
return <div className={`animate-[fadeUp_.55s_ease_both] ${className}`}>{children}</div>;
}

View File

@ -0,0 +1,9 @@
export function SectionHeading({ eyebrow, title, body }: { eyebrow: string; title: string; body: string }) {
return (
<div className="mx-auto max-w-3xl text-center">
<p className="text-sm font-bold uppercase tracking-[0.18em] text-forest dark:text-mint">{eyebrow}</p>
<h2 className="mt-3 font-display text-4xl text-ink dark:text-paper md:text-5xl">{title}</h2>
<p className="mt-4 text-lg leading-8 text-ink/68 dark:text-paper/68">{body}</p>
</div>
);
}

27
lib/data.ts Normal file
View File

@ -0,0 +1,27 @@
export const personas = [
["Mint Orphan", "Rock-solid sync, honest pricing, undo on everything."],
["Dual-Income Couple", "Shared dashboards with privacy controls and expense splitting."],
["Credit Builder", "Real FICO 8 and spending-linked coaching without referral bias."],
["Rewards Optimizer", "Card suggestions powered by actual transaction behavior."],
["DIY Investor", "Portfolio analysis across brokerage, 401k, and IRA accounts."]
] as const;
export const prices = [
["Free", "$0", "Core tracking for getting started"],
["Core", "$6.99", "Budgets, net worth, and subscriptions"],
["Plus", "$11.99", "FICO 8, Ask Aartha, rewards intelligence"],
["Premium", "$16.99", "Tri-bureau monitoring and dark-web alerts"]
] as const;
export const transactions = [
["Whole Foods", "Groceries", "-$82.14", "Today"],
["Blue Bottle", "Dining", "-$6.80", "Yesterday"],
["Payroll", "Income", "+$4,920.00", "Jul 1"],
["Spotify", "Subscription", "-$10.99", "Jun 30"]
] as const;
export const goals = [
{ name: "Emergency fund", saved: 8400, target: 12000, date: "Oct 2026" },
{ name: "Japan trip", saved: 3100, target: 6000, date: "Feb 2027" },
{ name: "New home down payment", saved: 42500, target: 90000, date: "Aug 2028" }
];

11
next.config.mjs Normal file
View File

@ -0,0 +1,11 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// Emit a fully static site into `out/` when `npm run build` is executed.
output: "export",
trailingSlash: true,
images: {
unoptimized: true,
},
};
export default nextConfig;

6350
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "aartha-react",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"preview": "npx serve out",
"lint": "next lint"
},
"dependencies": {
"clsx": "^2.1.1",
"lucide-react": "^0.468.0",
"next": "14.2.35",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
"eslint-config-next": "14.2.35",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.13",
"typescript": "^5"
}
}

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
export default config;

40
tailwind.config.ts Normal file
View File

@ -0,0 +1,40 @@
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: "class",
content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./lib/**/*.{ts,tsx}"],
theme: {
extend: {
colors: {
ink: "#10231D",
forest: "#0F6E56",
mint: "#1D9E75",
cream: "#F6F0E7",
paper: "#FFFDF8",
saffron: "#E8A23A",
coral: "#E56A54",
indigo: "#7F77DD"
},
boxShadow: {
soft: "0 24px 80px rgba(16, 35, 29, 0.12)",
lift: "0 14px 36px rgba(15, 110, 86, 0.16)"
},
fontFamily: {
sans: ["var(--font-inter)", "Inter", "system-ui", "sans-serif"],
display: ["var(--font-display)", "Georgia", "serif"]
},
keyframes: {
fadeUp: {
"0%": { opacity: "0", transform: "translateY(12px)" },
"100%": { opacity: "1", transform: "translateY(0)" }
}
},
animation: {
fadeUp: "fadeUp .55s ease both"
}
}
},
plugins: []
};
export default config;

24
tsconfig.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}