From Mohan Initial Push
This commit is contained in:
parent
200f442381
commit
f4b5e4bc74
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal 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
31
app/app/page.tsx
Normal 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
3
app/dashboard/page.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
export { default } from "../app/page";
|
||||||
49
app/globals.css
Normal file
49
app/globals.css
Normal 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
29
app/layout.tsx
Normal 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
23
app/mobile/page.tsx
Normal 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
16
app/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
components/charts/AreaChart.tsx
Normal file
38
components/charts/AreaChart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
components/charts/DonutChart.tsx
Normal file
25
components/charts/DonutChart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
components/charts/GaugeChart.tsx
Normal file
14
components/charts/GaugeChart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
components/charts/MiniBars.tsx
Normal file
10
components/charts/MiniBars.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
components/dashboard/AskAartha.tsx
Normal file
34
components/dashboard/AskAartha.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
components/dashboard/Sidebar.tsx
Normal file
42
components/dashboard/Sidebar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
components/dashboard/Topbar.tsx
Normal file
21
components/dashboard/Topbar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
138
components/dashboard/views.tsx
Normal file
138
components/dashboard/views.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
components/finance/FactorRow.tsx
Normal file
11
components/finance/FactorRow.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
components/finance/GoalCard.tsx
Normal file
21
components/finance/GoalCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
components/finance/InsightCard.tsx
Normal file
12
components/finance/InsightCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
components/finance/TransactionRow.tsx
Normal file
16
components/finance/TransactionRow.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
components/marketing/Hero.tsx
Normal file
48
components/marketing/Hero.tsx
Normal 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'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
components/marketing/Navbar.tsx
Normal file
19
components/marketing/Navbar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
78
components/marketing/Sections.tsx
Normal file
78
components/marketing/Sections.tsx
Normal 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'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>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
components/mobile/MobileScreens.tsx
Normal file
45
components/mobile/MobileScreens.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
components/mobile/PhoneFrame.tsx
Normal file
13
components/mobile/PhoneFrame.tsx
Normal 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
37
components/ui/Button.tsx
Normal 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
27
components/ui/Card.tsx
Normal 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
12
components/ui/Chip.tsx
Normal 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
11
components/ui/Delta.tsx
Normal 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
11
components/ui/Logo.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
components/ui/ProgressBar.tsx
Normal file
16
components/ui/ProgressBar.tsx
Normal 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
5
components/ui/Reveal.tsx
Normal 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>;
|
||||||
|
}
|
||||||
9
components/ui/SectionHeading.tsx
Normal file
9
components/ui/SectionHeading.tsx
Normal 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
27
lib/data.ts
Normal 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
11
next.config.mjs
Normal 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
6350
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal 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
8
postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
40
tailwind.config.ts
Normal file
40
tailwind.config.ts
Normal 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
24
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user