1052 lines
55 KiB
TypeScript
1052 lines
55 KiB
TypeScript
import InputError from '@/components/input-error';
|
|
import InstallAppButton from '@/components/landing/install-app-button';
|
|
import Header from '@/components/partials/header';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Spinner } from '@/components/ui/spinner';
|
|
import { usePwaInstall } from '@/hooks/use-pwa-install';
|
|
import { cn } from '@/lib/utils';
|
|
import { store as storeUserLead } from '@/routes/user-leads';
|
|
import { type SharedData } from '@/types';
|
|
import { Plan } from '@/types/pricing';
|
|
import { formatCurrency } from '@/utils/currency';
|
|
import { __ } from '@/utils/i18n';
|
|
import { Form, Head, Link, router, usePage } from '@inertiajs/react';
|
|
import { CheckIcon, ChevronDownIcon, LockIcon } from 'lucide-react';
|
|
import { type ReactNode, useEffect, useState } from 'react';
|
|
|
|
const LANDING_IMAGES = [
|
|
{
|
|
key: 'bank-accounts',
|
|
light: '/images/landing/whisper.money_light_3.png',
|
|
dark: '/images/landing/whisper.money_dark_3.png',
|
|
alt: 'Your transactions at a glance',
|
|
className: 'left-[-24%] group-hover:left-[-32%]',
|
|
},
|
|
{
|
|
key: 'unlock-key',
|
|
light: '/images/landing/whisper.money_light_2.png',
|
|
dark: '/images/landing/whisper.money_dark_2.png',
|
|
alt: 'Manage all your accounts in a single place',
|
|
className: '',
|
|
},
|
|
{
|
|
key: 'transactions',
|
|
light: '/images/landing/whisper.money_light_1.png',
|
|
dark: '/images/landing/whisper.money_dark_1.png',
|
|
alt: 'Analyze your money, how it evolves, and how do you spent it',
|
|
className: 'left-[32%] group-hover:left-[48%]',
|
|
},
|
|
] as const;
|
|
|
|
function getBillingLabel(billingPeriod: string | null): string {
|
|
if (!billingPeriod) {
|
|
return 'one-time';
|
|
}
|
|
return `/${billingPeriod}`;
|
|
}
|
|
|
|
function FeatureScreenshot({
|
|
light,
|
|
dark,
|
|
alt,
|
|
className,
|
|
}: {
|
|
light: string;
|
|
dark: string;
|
|
alt: string;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'overflow-hidden rounded-xl border border-[#e3e3e0]/70 bg-gradient-to-br from-zinc-50 to-zinc-100 dark:border-[#3E3E3A]/30 dark:from-zinc-900 dark:to-zinc-950',
|
|
className,
|
|
)}
|
|
>
|
|
<img
|
|
src={light}
|
|
alt={alt}
|
|
className="w-full dark:hidden"
|
|
loading="lazy"
|
|
/>
|
|
<img
|
|
src={dark}
|
|
alt={alt}
|
|
className="hidden w-full dark:block"
|
|
loading="lazy"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function FeatureCard({
|
|
children,
|
|
className,
|
|
}: {
|
|
children: ReactNode;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'overflow-hidden rounded-2xl border border-[#e3e3e0] bg-[#FDFDFC] dark:border-[#3E3E3A] dark:bg-[#161615]',
|
|
className,
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function LandingPlanCard({
|
|
plan,
|
|
isDefault,
|
|
isBestValue,
|
|
currency,
|
|
locale,
|
|
}: {
|
|
plan: Plan;
|
|
isDefault: boolean;
|
|
isBestValue: boolean;
|
|
promoEnabled: boolean;
|
|
promoBadge: string;
|
|
currency: string;
|
|
locale: string;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex flex-col overflow-hidden rounded-2xl border border-[#e3e3e0] bg-[#FDFDFC] dark:border-[#3E3E3A] dark:bg-[#161615]',
|
|
isDefault && 'ring-2 ring-[#1b1b18] dark:ring-[#EDEDEC]',
|
|
)}
|
|
>
|
|
{(isDefault || isBestValue) && (
|
|
<div
|
|
className={cn(
|
|
'px-6 pt-6 text-xs font-semibold uppercase',
|
|
isDefault && 'text-[#1b1b18] dark:text-[#aaa]',
|
|
isBestValue &&
|
|
!isDefault &&
|
|
'text-[#706f6c] dark:text-[#A1A09A]',
|
|
)}
|
|
>
|
|
{isDefault ? __('Most Popular') : __('Best Value')}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-1 flex-col p-6 pt-2">
|
|
<h3 className="text-lg font-semibold">{__(plan.name)}</h3>
|
|
|
|
<div className="mt-3 flex items-baseline gap-2">
|
|
{plan.original_price && (
|
|
<span className="text-lg font-medium text-[#706f6c] line-through dark:text-[#A1A09A]">
|
|
{formatCurrency(
|
|
plan.original_price * 100,
|
|
currency,
|
|
locale,
|
|
)}
|
|
</span>
|
|
)}
|
|
<span className="text-4xl font-bold tracking-tight">
|
|
{formatCurrency(plan.price * 100, currency, locale)}
|
|
</span>
|
|
<span className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
|
{getBillingLabel(plan.billing_period)}
|
|
</span>
|
|
</div>
|
|
|
|
<p className="mt-3 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
|
{__(
|
|
'Everything you need to manage your finances securely.',
|
|
)}
|
|
</p>
|
|
|
|
<div className="my-5 h-px bg-[#e3e3e0] dark:bg-[#3E3E3A]" />
|
|
|
|
<ul className="flex-1 space-y-2.5">
|
|
{plan.features.map((feature) => (
|
|
<li key={feature} className="flex items-center gap-2.5">
|
|
<CheckIcon className="size-4 shrink-0 text-[#1b1b18] dark:text-[#EDEDEC]" />
|
|
<span className="text-sm">{__(feature)}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<Link href="/register" className="mt-8">
|
|
<Button
|
|
className={cn(
|
|
'w-full cursor-pointer py-5 text-base shadow-sm transition-all',
|
|
isDefault
|
|
? 'bg-gradient-to-t from-zinc-700 to-zinc-900 text-white hover:from-zinc-800 hover:to-black hover:shadow-md dark:from-zinc-200 dark:to-zinc-300 dark:text-[#1C1C1A] hover:dark:from-zinc-50'
|
|
: 'border-[#e3e3e0] bg-transparent text-[#1b1b18] hover:bg-[#f5f5f4] dark:border-[#3E3E3A] dark:text-[#EDEDEC] dark:hover:bg-[#1f1f1e]',
|
|
)}
|
|
variant={isDefault ? 'default' : 'outline'}
|
|
>
|
|
{__('Get Started')}
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<div className="border-b border-[#e3e3e0] dark:border-[#3E3E3A]">
|
|
<button
|
|
type="button"
|
|
className="flex w-full cursor-pointer items-center justify-between py-5 text-left"
|
|
onClick={() => setOpen(!open)}
|
|
>
|
|
<span className="text-base font-medium">{question}</span>
|
|
<ChevronDownIcon
|
|
className={cn(
|
|
'size-5 shrink-0 text-[#706f6c] transition-transform duration-200 dark:text-[#A1A09A]',
|
|
open && 'rotate-180',
|
|
)}
|
|
/>
|
|
</button>
|
|
<div
|
|
className={cn(
|
|
'grid transition-all duration-200',
|
|
open ? 'grid-rows-[1fr] pb-5' : 'grid-rows-[0fr]',
|
|
)}
|
|
>
|
|
<div className="overflow-hidden">
|
|
<p className="text-sm leading-relaxed text-[#706f6c] dark:text-[#A1A09A]">
|
|
{answer}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function WaitlistForm() {
|
|
const [referrerCode, setReferrerCode] = useState('');
|
|
const { locale } = usePage<SharedData>().props;
|
|
|
|
useEffect(() => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const ref = params.get('ref');
|
|
if (ref) {
|
|
setReferrerCode(ref);
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<Form
|
|
{...storeUserLead.form()}
|
|
className="flex w-full flex-col gap-3"
|
|
disableWhileProcessing
|
|
>
|
|
{({ processing, errors }) => (
|
|
<>
|
|
<input
|
|
type="hidden"
|
|
name="referrer_code"
|
|
value={referrerCode}
|
|
/>
|
|
<input type="hidden" name="locale" value={locale} />
|
|
<div className="flex w-full flex-col gap-1.5">
|
|
<div className="flex w-full flex-row gap-2">
|
|
<Input
|
|
type="email"
|
|
name="email"
|
|
required
|
|
autoComplete="email"
|
|
placeholder={__('Your email address')}
|
|
className="h-14 flex-1 text-base"
|
|
/>
|
|
<Button
|
|
type="submit"
|
|
className="text-shadow h-14 shrink-0 cursor-pointer bg-gradient-to-t from-zinc-700 to-zinc-900 px-6 text-base text-white shadow-sm transition-all duration-200 hover:from-zinc-800 hover:to-black hover:shadow-md dark:from-zinc-200 dark:to-zinc-300 dark:text-[#1C1C1A] dark:hover:from-zinc-50"
|
|
>
|
|
{processing && <Spinner />}
|
|
{__('Join Waitlist')}
|
|
</Button>
|
|
</div>
|
|
<InputError message={errors.email} />
|
|
</div>
|
|
</>
|
|
)}
|
|
</Form>
|
|
);
|
|
}
|
|
|
|
export default function Welcome({
|
|
canRegister,
|
|
hideAuthButtons,
|
|
}: {
|
|
canRegister?: boolean;
|
|
hideAuthButtons?: boolean;
|
|
}) {
|
|
const { appUrl, subscriptionsEnabled, pricing, locale } =
|
|
usePage<SharedData>().props;
|
|
const planEntries = Object.entries(pricing.plans);
|
|
const { isMobile } = usePwaInstall();
|
|
|
|
// Handle localStorage for language preference
|
|
useEffect(() => {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const langParam = urlParams.get('lang');
|
|
|
|
if (langParam && ['en', 'es'].includes(langParam)) {
|
|
// Store the preference in localStorage
|
|
localStorage.setItem('whisper_landing_locale', langParam);
|
|
} else {
|
|
// No query param - check if we have a stored preference
|
|
const storedLocale = localStorage.getItem('whisper_landing_locale');
|
|
|
|
if (
|
|
storedLocale &&
|
|
storedLocale !== locale &&
|
|
['en', 'es'].includes(storedLocale)
|
|
) {
|
|
// Redirect to stored preference
|
|
window.location.href = `/?lang=${storedLocale}`;
|
|
return;
|
|
} else if (!storedLocale && locale) {
|
|
// First visit - store the detected locale from session/header
|
|
localStorage.setItem('whisper_landing_locale', locale);
|
|
}
|
|
}
|
|
}, [locale]);
|
|
|
|
const [isPwa] = useState(() => {
|
|
if (typeof window === 'undefined') {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
window.matchMedia('(display-mode: standalone)').matches ||
|
|
('standalone' in navigator &&
|
|
(navigator as Navigator & { standalone: boolean }).standalone)
|
|
);
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (isPwa) {
|
|
router.visit('/dashboard');
|
|
}
|
|
}, [isPwa]);
|
|
|
|
if (isPwa) {
|
|
return (
|
|
<div className="flex h-screen items-center justify-center">
|
|
<Spinner className="size-8" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Head
|
|
title={__(
|
|
'Whisper Money - The Most Secure Personal Finance App',
|
|
)}
|
|
>
|
|
<meta
|
|
name="description"
|
|
content={__(
|
|
'The most secure privacy-first personal finance app. Track expenses, create budgets, and manage your money privately.',
|
|
)}
|
|
/>
|
|
|
|
<meta
|
|
name="keywords"
|
|
content={__(
|
|
'finance app, budgeting, expense tracking, secure finance, personal finance, money management, privacy, privacy-first finance app',
|
|
)}
|
|
/>
|
|
|
|
<link rel="canonical" href={appUrl} />
|
|
|
|
<meta property="og:site_name" content="Whisper Money" />
|
|
<meta
|
|
property="og:title"
|
|
content={__(
|
|
'Whisper Money - The Most Secure Personal Finance App',
|
|
)}
|
|
/>
|
|
|
|
<meta
|
|
property="og:description"
|
|
content={__(
|
|
'Your financial data stays private. The most secure way to manage your personal finances.',
|
|
)}
|
|
/>
|
|
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:url" content={appUrl} />
|
|
<meta
|
|
property="og:image"
|
|
content={`${appUrl}/images/og_whisper_money.png`}
|
|
/>
|
|
|
|
<meta property="og:image:width" content="1200" />
|
|
<meta property="og:image:height" content="630" />
|
|
<meta
|
|
property="og:image:alt"
|
|
content={__('Whisper Money - Secure Personal Finance App')}
|
|
/>
|
|
|
|
<meta property="og:locale" content={__('en_US')} />
|
|
|
|
<meta name="twitter:card" content={__('summary_large_image')} />
|
|
<meta
|
|
name="twitter:title"
|
|
content={__(
|
|
'Whisper Money - The Most Secure Personal Finance App',
|
|
)}
|
|
/>
|
|
|
|
<meta
|
|
name="twitter:description"
|
|
content={__(
|
|
'Your financial data stays private. The most secure way to manage your personal finances.',
|
|
)}
|
|
/>
|
|
|
|
<meta
|
|
name="twitter:image"
|
|
content={`${appUrl}/images/og_whisper_money.png`}
|
|
/>
|
|
|
|
<meta
|
|
name="twitter:image:alt"
|
|
content={__('Whisper Money - Secure Personal Finance App')}
|
|
/>
|
|
|
|
<script type="application/ld+json">
|
|
{JSON.stringify({
|
|
'@context': 'https://schema.org',
|
|
'@type': 'WebApplication',
|
|
name: 'Whisper Money',
|
|
description:
|
|
'The most secure privacy-first personal finance app. Track expenses, create budgets, and manage your money privately.',
|
|
url: appUrl,
|
|
applicationCategory: 'FinanceApplication',
|
|
featureList: [
|
|
'Privacy-first design',
|
|
'Smart budgeting',
|
|
'Expense tracking',
|
|
'Visual insights',
|
|
'Zero tracking',
|
|
'Open source',
|
|
],
|
|
})}
|
|
</script>
|
|
</Head>
|
|
<div className="flex min-h-screen flex-col bg-[#FDFDFC] text-[#1b1b18] dark:bg-[#0a0a0a] dark:text-[#EDEDEC]">
|
|
<Header
|
|
canRegister={canRegister}
|
|
hideAuthButtons={hideAuthButtons}
|
|
/>
|
|
|
|
<main className="flex flex-1 flex-col">
|
|
<section className="relative w-full overflow-hidden px-6 py-28 sm:py-32 md:py-40">
|
|
<div className="relative z-10 mx-auto flex max-w-7xl flex-col gap-12">
|
|
<div className="relative z-10 mx-auto flex w-full max-w-5xl flex-col items-start gap-6 sm:gap-8">
|
|
<span className="inline-flex items-center gap-2 rounded-full border border-[#e3e3e0] px-2.5 py-1 text-[0.8rem] font-medium dark:border-[#3E3E3A]">
|
|
<LockIcon className="size-3.5 opacity-75" />
|
|
<span className="text-[#706f6c] dark:text-[#A1A09A]">
|
|
{__('Private & Secure')}
|
|
</span>
|
|
</span>
|
|
<h1 className="font-heading max-w-[840px] bg-gradient-to-r from-[#1b1b18] to-[#1b1b18] bg-clip-text text-4xl leading-tight font-semibold text-balance text-transparent drop-shadow-2xl sm:text-5xl sm:leading-tight lg:text-6xl lg:leading-tight dark:from-[#EDEDEC] dark:to-[#A1A09A]">
|
|
{__(
|
|
'All your money in one place. No spreadsheets. Private.',
|
|
)}
|
|
</h1>
|
|
<p className="mb-4 max-w-[840px] text-lg leading-8 font-medium text-balance text-[#706f6c] lg:text-xl lg:leading-8 dark:text-[#A1A09A]">
|
|
{__(
|
|
'Understand your finances and make better decisions without the friction. Track expenses, create budgets, and achieve your goals\u2014all in one place.',
|
|
)}
|
|
</p>
|
|
<div className="flex w-full max-w-lg flex-col gap-4">
|
|
{hideAuthButtons ? (
|
|
<WaitlistForm />
|
|
) : isMobile ? (
|
|
<InstallAppButton />
|
|
) : (
|
|
<div className="flex w-full flex-row gap-4">
|
|
<Link
|
|
href="/register"
|
|
className="w-full"
|
|
>
|
|
<Button className="text-shadow duration h-14 w-full cursor-pointer bg-gradient-to-t from-zinc-700 to-zinc-900 text-base text-white shadow-sm transition-all hover:from-zinc-800 hover:to-black hover:shadow-md dark:bg-[#eeeeec] dark:from-zinc-200 dark:to-zinc-300 dark:text-[#1C1C1A] dark:hover:bg-white hover:dark:from-zinc-50 dark:hover:shadow-md">
|
|
{__('Get Started')}
|
|
</Button>
|
|
</Link>
|
|
<Link href="/login?demo=1">
|
|
<Button
|
|
variant={'secondary'}
|
|
size={'lg'}
|
|
className="h-14"
|
|
>
|
|
{__('Check Demo')}
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
<p className="text-xs text-[#706f6c] dark:text-[#A1A09A]">
|
|
{hideAuthButtons
|
|
? __(
|
|
"Join the waiting list. We'll let you know when you're in.",
|
|
)
|
|
: __(
|
|
'Your data stays private. Always.',
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="group relative sm:px-24">
|
|
{LANDING_IMAGES.map((image, index) => (
|
|
<div
|
|
key={image.key}
|
|
className={cn(
|
|
'relative z-10 transition-all delay-200 duration-700 ease-in-out',
|
|
'rotate-[-24deg] skew-y-12 group-hover:rotate-[-12deg] group-hover:skew-y-6',
|
|
'max-sm:rotate-[-16deg] max-sm:skew-y-8 max-sm:group-hover:rotate-[-8deg] max-sm:group-hover:skew-y-4',
|
|
index < LANDING_IMAGES.length - 1 &&
|
|
'h-[24px]',
|
|
image.className,
|
|
)}
|
|
>
|
|
<div className="relative z-10 overflow-hidden rounded-2xl border border-[#e3e3e0]/50 bg-[#FDFDFC]/50 p-2 shadow-2xl dark:border-[#3E3E3A]/10 dark:bg-[#161615]/50">
|
|
<div className="relative z-10 overflow-hidden rounded-md border border-[#e3e3e0]/70 shadow-2xl dark:border-[#3E3E3A]/5">
|
|
<div className="rounded-lg border-[#e3e3e0] bg-[#FDFDFC] dark:border-[#3E3E3A] dark:bg-[#0a0a0a]">
|
|
<img
|
|
src={image.light}
|
|
alt={image.alt}
|
|
className="w-full rounded-lg dark:hidden"
|
|
/>
|
|
|
|
<img
|
|
src={image.dark}
|
|
alt={image.alt}
|
|
className="hidden w-full rounded-lg dark:block"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<div
|
|
data-slot="glow"
|
|
className="animate-appear-zoom absolute top-[50%] mt-32 w-full opacity-0 delay-2000 lg:mt-4"
|
|
>
|
|
<div className="absolute left-1/2 h-[256px] w-[60%] -translate-x-1/2 -translate-y-1/2 scale-[2.5] rounded-[50%] bg-radial from-[#1b1b18]/50 from-10% to-[#1b1b18]/0 to-60% opacity-20 sm:h-[512px] dark:from-[#EDEDEC]/50 dark:to-[#EDEDEC]/0 dark:opacity-100"></div>
|
|
<div className="absolute left-1/2 h-[128px] w-[40%] -translate-x-1/2 -translate-y-1/2 scale-200 rounded-[50%] bg-radial from-[#1b1b18]/30 from-10% to-[#1b1b18]/0 to-60% opacity-20 sm:h-[256px] dark:from-[#EDEDEC]/30 dark:to-[#EDEDEC]/0 dark:opacity-100"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="grid gap-6 px-4 py-12 sm:py-16 md:py-20">
|
|
<div className="mx-auto grid max-w-7xl gap-6 sm:grid-cols-3">
|
|
<FeatureCard>
|
|
<div className="p-2">
|
|
<FeatureScreenshot
|
|
light="/images/landing/features/accounts_light.png"
|
|
dark="/images/landing/features/accounts_dark.png"
|
|
alt={__(
|
|
'All your accounts at a glance',
|
|
)}
|
|
/>
|
|
</div>
|
|
<div className="p-6 pt-4">
|
|
<h3 className="text-xl font-semibold">
|
|
{__('All Your Accounts')}
|
|
</h3>
|
|
<p className="mt-2 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
|
{__(
|
|
'See every account in one place. Track balances, monitor changes, and always know where you stand.',
|
|
)}
|
|
</p>
|
|
</div>
|
|
</FeatureCard>
|
|
|
|
<FeatureCard>
|
|
<div className="p-2">
|
|
<FeatureScreenshot
|
|
light="/images/landing/features/transactions_light.png"
|
|
dark="/images/landing/features/transactions_dark.png"
|
|
alt={__('Every transaction tracked')}
|
|
/>
|
|
</div>
|
|
<div className="p-6 pt-4">
|
|
<h3 className="text-xl font-semibold">
|
|
{__('Every Transaction')}
|
|
</h3>
|
|
<p className="mt-2 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
|
{__(
|
|
'Search, filter, and categorize with ease. Understand exactly where your money goes.',
|
|
)}
|
|
</p>
|
|
</div>
|
|
</FeatureCard>
|
|
|
|
<FeatureCard>
|
|
<div className="p-2">
|
|
<FeatureScreenshot
|
|
light="/images/landing/features/privacy_light.png"
|
|
dark="/images/landing/features/privacy_dark.png"
|
|
alt={__(
|
|
'Your data encrypted and private',
|
|
)}
|
|
/>
|
|
</div>
|
|
<div className="p-6 pt-4">
|
|
<h3 className="text-xl font-semibold">
|
|
{__('Your Data, Your Rules')}
|
|
</h3>
|
|
<p className="mt-2 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
|
{__(
|
|
'No third-party sharing, no AI snooping. Your financial data belongs to you and only you.',
|
|
)}
|
|
</p>
|
|
</div>
|
|
</FeatureCard>
|
|
</div>
|
|
|
|
<div className="mx-auto max-w-7xl">
|
|
<FeatureCard>
|
|
<div className="grid items-center gap-0 sm:grid-cols-2">
|
|
<div className="p-8 sm:p-12">
|
|
<h2 className="text-3xl leading-tight font-semibold sm:text-4xl sm:leading-tight">
|
|
{__('Import in Seconds')}
|
|
</h2>
|
|
<p className="mt-4 text-[#706f6c] dark:text-[#A1A09A]">
|
|
{__(
|
|
"Export a CSV or XLS from your bank and drag it in. A year's worth of transactions imported in under 10 seconds.",
|
|
)}
|
|
</p>
|
|
<ul className="mt-6 space-y-3">
|
|
<li className="flex items-center gap-2.5">
|
|
<CheckIcon className="size-4 shrink-0 text-emerald-500" />
|
|
<span className="text-sm">
|
|
{__('Export from any bank')}
|
|
</span>
|
|
</li>
|
|
<li className="flex items-center gap-2.5">
|
|
<CheckIcon className="size-4 shrink-0 text-emerald-500" />
|
|
<span className="text-sm">
|
|
{__('Secure upload')}
|
|
</span>
|
|
</li>
|
|
<li className="flex items-center gap-2.5">
|
|
<CheckIcon className="size-4 shrink-0 text-emerald-500" />
|
|
<span className="text-sm">
|
|
{__(
|
|
'Automatic categorization',
|
|
)}
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div className="p-2">
|
|
<FeatureScreenshot
|
|
light="/images/landing/features/import_light.png"
|
|
dark="/images/landing/features/import_dark.png"
|
|
alt={__(
|
|
'Import transactions in seconds',
|
|
)}
|
|
className="aspect-auto min-h-[320px]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</FeatureCard>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="px-4 py-12 sm:py-16 md:py-20">
|
|
<div className="mx-auto flex max-w-7xl flex-col gap-8 sm:gap-12">
|
|
<div className="flex flex-col items-center gap-4 text-center">
|
|
<h2 className="max-w-[720px] text-3xl leading-tight font-semibold sm:text-5xl sm:leading-tight">
|
|
{__('Smart Budgets')}
|
|
</h2>
|
|
<p className="text-md max-w-[640px] font-medium text-[#706f6c] sm:text-xl dark:text-[#A1A09A]">
|
|
{__(
|
|
'Create budgets that adapt to your spending habits and help you reach your goals.',
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid gap-6 sm:grid-cols-3">
|
|
<FeatureCard>
|
|
<div className="p-2">
|
|
<FeatureScreenshot
|
|
light="/images/landing/features/budgets_light.png"
|
|
dark="/images/landing/features/budgets_dark.png"
|
|
alt={__('Set your budget goals')}
|
|
/>
|
|
</div>
|
|
<div className="p-6 pt-4">
|
|
<h3 className="text-xl font-semibold">
|
|
{__('Set Your Goals')}
|
|
</h3>
|
|
<p className="mt-2 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
|
{__(
|
|
'Define monthly budgets by category. Know exactly how much you can spend.',
|
|
)}
|
|
</p>
|
|
</div>
|
|
</FeatureCard>
|
|
|
|
<FeatureCard>
|
|
<div className="p-2">
|
|
<FeatureScreenshot
|
|
light="/images/landing/features/budget_detail_light.png"
|
|
dark="/images/landing/features/budget_detail_dark.png"
|
|
alt={__(
|
|
'Track your budget progress',
|
|
)}
|
|
/>
|
|
</div>
|
|
<div className="p-6 pt-4">
|
|
<h3 className="text-xl font-semibold">
|
|
{__('Track Progress')}
|
|
</h3>
|
|
<p className="mt-2 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
|
{__(
|
|
'See where you stand in real-time. Visual progress bars show spending vs. budget.',
|
|
)}
|
|
</p>
|
|
</div>
|
|
</FeatureCard>
|
|
|
|
<FeatureCard>
|
|
<div className="p-2">
|
|
<FeatureScreenshot
|
|
light="/images/landing/features/budget_edit_light.png"
|
|
dark="/images/landing/features/budget_edit_dark.png"
|
|
alt={__(
|
|
'Budget insights and alerts',
|
|
)}
|
|
/>
|
|
</div>
|
|
<div className="p-6 pt-4">
|
|
<h3 className="text-xl font-semibold">
|
|
{__('Stay on Track')}
|
|
</h3>
|
|
<p className="mt-2 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
|
{__(
|
|
"Get notified when you're close to your limit. Never overspend again.",
|
|
)}
|
|
</p>
|
|
</div>
|
|
</FeatureCard>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="px-4 py-12 sm:py-16 md:py-20">
|
|
<div className="mx-auto max-w-7xl">
|
|
<FeatureCard>
|
|
<div className="grid items-center gap-0 sm:grid-cols-2">
|
|
<div className="p-2">
|
|
<FeatureScreenshot
|
|
light="/images/landing/features/cashflow_light.png"
|
|
dark="/images/landing/features/cashflow_dark.png"
|
|
alt={__('Cashflow visualization')}
|
|
className="aspect-auto min-h-[320px]"
|
|
/>
|
|
</div>
|
|
<div className="p-8 sm:p-12">
|
|
<h2 className="text-3xl leading-tight font-semibold sm:text-4xl sm:leading-tight">
|
|
{__('Cashflow at a Glance')}
|
|
</h2>
|
|
<p className="mt-4 text-[#706f6c] dark:text-[#A1A09A]">
|
|
{__(
|
|
'Visualize your money flow over time. See income vs. expenses and spot trends before they become problems.',
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</FeatureCard>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="w-full overflow-hidden px-0 py-12 sm:py-16 md:py-20">
|
|
<div className="mx-auto flex max-w-7xl flex-col items-center gap-8 px-4 text-center sm:gap-12">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<h2 className="max-w-[720px] text-3xl leading-tight font-semibold text-balance sm:text-4xl sm:leading-tight">
|
|
{__(
|
|
'Trusted by people who value their privacy',
|
|
)}
|
|
</h2>
|
|
<p className="text-md max-w-[600px] font-medium text-[#706f6c] sm:text-lg dark:text-[#A1A09A]">
|
|
{__(
|
|
'Join thousands of users who have taken control of their finances without compromising their privacy.',
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="relative w-full">
|
|
<div className="group flex flex-row [gap:var(--gap)] overflow-hidden p-2 [--duration:25s] [--gap:1rem]">
|
|
{[0, 1].map((copy) => (
|
|
<div
|
|
key={copy}
|
|
className="animate-marquee flex shrink-0 flex-row justify-around [gap:var(--gap)] group-hover:[animation-play-state:paused]"
|
|
>
|
|
{[
|
|
{
|
|
name: __('Sarah M.'),
|
|
handle: '@sarahm_finance',
|
|
text: __(
|
|
"Finally, a finance app that respects my privacy. Knowing my data isn't being shared gives me peace of mind.",
|
|
),
|
|
},
|
|
{
|
|
name: __('Michael R.'),
|
|
handle: '@mike_tech',
|
|
text: __(
|
|
"The budgeting features are intuitive and the dark mode is gorgeous. Best finance app I've used.",
|
|
),
|
|
},
|
|
{
|
|
name: __('Emma L.'),
|
|
handle: '@emmalou',
|
|
text: __(
|
|
'Love that my financial data stays private. No more worrying about who has access to my spending habits!',
|
|
),
|
|
},
|
|
{
|
|
name: __('David K.'),
|
|
handle: '@davidk_dev',
|
|
text: __(
|
|
'As a developer, I appreciate the security architecture. This is how finance apps should be built.',
|
|
),
|
|
},
|
|
{
|
|
name: __('Jessica P.'),
|
|
handle: '@jessicap',
|
|
text: __(
|
|
'The automation rules save me so much time. And knowing my data is private? Priceless.',
|
|
),
|
|
},
|
|
{
|
|
name: __('Alex T.'),
|
|
handle: '@alext_money',
|
|
text: __(
|
|
'Clean interface, powerful features, and zero compromise on privacy. What more could you want?',
|
|
),
|
|
},
|
|
].map((testimonial) => (
|
|
<div
|
|
key={`${copy}-${testimonial.handle}`}
|
|
className="flex w-[300px] shrink-0 flex-col rounded-2xl border border-[#e3e3e0] bg-[#FDFDFC] p-6 text-start shadow-sm sm:w-[360px] dark:border-[#3E3E3A] dark:bg-[#161615]"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex size-10 items-center justify-center rounded-full bg-zinc-100 text-sm font-semibold text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">
|
|
{testimonial.name.charAt(
|
|
0,
|
|
)}
|
|
</div>
|
|
<div className="flex flex-col items-start">
|
|
<h3 className="text-sm leading-none font-semibold">
|
|
{
|
|
testimonial.name
|
|
}
|
|
</h3>
|
|
<p className="mt-1 text-xs text-[#706f6c] dark:text-[#A1A09A]">
|
|
{
|
|
testimonial.handle
|
|
}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<p className="mt-4 text-sm leading-relaxed text-[#706f6c] dark:text-[#A1A09A]">
|
|
{testimonial.text}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="pointer-events-none absolute inset-y-0 left-0 hidden w-1/6 bg-linear-to-r from-[#FDFDFC] sm:block dark:from-[#0a0a0a]"></div>
|
|
<div className="pointer-events-none absolute inset-y-0 right-0 hidden w-1/6 bg-linear-to-l from-[#FDFDFC] sm:block dark:from-[#0a0a0a]"></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{subscriptionsEnabled &&
|
|
!hideAuthButtons &&
|
|
planEntries.length > 0 && (
|
|
<section
|
|
id="pricing"
|
|
className="px-4 py-12 sm:py-16 md:py-20"
|
|
>
|
|
<div className="mx-auto flex max-w-5xl flex-col items-center gap-8 sm:gap-12">
|
|
<div className="flex flex-col items-center gap-4 text-center">
|
|
<h2 className="text-3xl leading-tight font-semibold sm:text-5xl sm:leading-tight">
|
|
{__('Simple, transparent pricing')}
|
|
</h2>
|
|
<p className="text-md max-w-[600px] font-medium text-[#706f6c] sm:text-lg dark:text-[#A1A09A]">
|
|
{__(
|
|
'Choose the plan that works for you. No hidden fees.',
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
className={cn(
|
|
'grid w-full gap-6',
|
|
planEntries.length === 1 &&
|
|
'mx-auto max-w-md',
|
|
planEntries.length === 2 &&
|
|
'mx-auto max-w-3xl grid-cols-1 sm:grid-cols-2',
|
|
planEntries.length >= 3 &&
|
|
'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
|
|
)}
|
|
>
|
|
{planEntries.map(([key, plan]) => (
|
|
<LandingPlanCard
|
|
key={key}
|
|
plan={plan}
|
|
isDefault={
|
|
key === pricing.defaultPlan
|
|
}
|
|
isBestValue={
|
|
key ===
|
|
pricing.bestValuePlan
|
|
}
|
|
promoEnabled={
|
|
pricing.promo.enabled
|
|
}
|
|
promoBadge={pricing.promo.badge}
|
|
currency={pricing.currency}
|
|
locale={locale}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<section className="px-4 py-12 sm:py-16 md:py-20">
|
|
<div className="mx-auto max-w-3xl">
|
|
<div className="mb-8 flex flex-col items-center gap-4 text-center sm:mb-12">
|
|
<h2 className="text-3xl leading-tight font-semibold sm:text-5xl sm:leading-tight">
|
|
{__('Frequently Asked Questions')}
|
|
</h2>
|
|
<p className="text-md max-w-[600px] font-medium text-[#706f6c] sm:text-lg dark:text-[#A1A09A]">
|
|
{__(
|
|
'Everything you need to know before getting started.',
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="rounded-2xl border border-[#e3e3e0] bg-[#FDFDFC] px-6 dark:border-[#3E3E3A] dark:bg-[#161615]">
|
|
<FaqItem
|
|
question={__(
|
|
'How is my financial data kept private?',
|
|
)}
|
|
answer={__(
|
|
"Your data is stored securely and never shared with third parties. We don't use your financial data for AI training, advertising, or any purpose other than providing you the service.",
|
|
)}
|
|
/>
|
|
<FaqItem
|
|
question={__(
|
|
'Do you connect directly to my bank?',
|
|
)}
|
|
answer={__(
|
|
'No. We never ask for your bank credentials. You import transactions by exporting a CSV or XLS file from your bank and uploading it to Whisper Money. This keeps your bank account secure.',
|
|
)}
|
|
/>
|
|
<FaqItem
|
|
question={__(
|
|
'Can I export or delete my data?',
|
|
)}
|
|
answer={__(
|
|
'Absolutely. You own your data. You can export all your financial data at any time, and you can permanently delete your account and all associated data whenever you want.',
|
|
)}
|
|
/>
|
|
<FaqItem
|
|
question={__(
|
|
'What file formats are supported for import?',
|
|
)}
|
|
answer={__(
|
|
'We support CSV and XLS files. Most banks allow you to export your transaction history in one of these formats. The import process automatically maps columns and categorizes transactions.',
|
|
)}
|
|
/>
|
|
<FaqItem
|
|
question={__(
|
|
'Is Whisper Money open source?',
|
|
)}
|
|
answer={__(
|
|
'Yes! Whisper Money is fully open source. You can review the code, suggest improvements, or even self-host it. Transparency is a core part of our privacy commitment.',
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="px-4 py-12 sm:py-16 md:py-20">
|
|
<div className="mx-auto max-w-7xl">
|
|
<div className="flex flex-col items-center gap-6 px-6 py-12 text-center sm:px-12 sm:py-16 dark:border-[#3E3E3A] dark:bg-[#161615]">
|
|
<h2 className="max-w-[600px] text-3xl leading-tight font-semibold text-balance sm:text-4xl sm:leading-tight">
|
|
{__(
|
|
'Ready to take control of your finances?',
|
|
)}
|
|
</h2>
|
|
<p className="max-w-[480px] text-balance text-[#706f6c] sm:text-lg dark:text-[#A1A09A]">
|
|
{__(
|
|
'Start managing your money privately. No credit card required.',
|
|
)}
|
|
</p>
|
|
<div className="flex flex-col gap-3 sm:flex-row">
|
|
<Link href="/register">
|
|
<Button className="h-12 cursor-pointer bg-gradient-to-t from-zinc-700 to-zinc-900 px-8 text-base text-white shadow-sm transition-all hover:from-zinc-800 hover:to-black hover:shadow-md dark:from-zinc-200 dark:to-zinc-300 dark:text-[#1C1C1A] hover:dark:from-zinc-50">
|
|
{__('Get Started for Free')}
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<footer className="py-8 lg:mt-12 dark:border-[#3E3E3A]">
|
|
<div className="mx-auto flex max-w-5xl flex-col items-center justify-between gap-4 px-6 text-sm text-[#706f6c] sm:flex-row lg:px-8 dark:text-[#A1A09A]">
|
|
<p>
|
|
© {new Date().getFullYear()}
|
|
{__(
|
|
'Whisper Money. All\n rights reserved.',
|
|
)}
|
|
</p>
|
|
<div className="flex gap-6">
|
|
<Link
|
|
href="/privacy"
|
|
className="hover:text-[#1b1b18] dark:hover:text-[#EDEDEC]"
|
|
>
|
|
{__('Privacy Policy')}
|
|
</Link>
|
|
<Link
|
|
href="/terms"
|
|
className="hover:text-[#1b1b18] dark:hover:text-[#EDEDEC]"
|
|
>
|
|
{__('Terms of Service')}
|
|
</Link>
|
|
<a
|
|
href={`/?lang=${locale === 'es' ? 'en' : 'es'}`}
|
|
className="cursor-pointer hover:text-[#1b1b18] dark:hover:text-[#EDEDEC]"
|
|
>
|
|
{locale === 'es' ? 'English' : 'Español'}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|