feat: reformula landing page e experiência mobile
@@ -7,11 +7,6 @@ export const america = localFont({
|
|||||||
weight: "400",
|
weight: "400",
|
||||||
style: "normal",
|
style: "normal",
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// path: "./america-medium.woff2",
|
|
||||||
// weight: "500",
|
|
||||||
// style: "normal",
|
|
||||||
// },
|
|
||||||
],
|
],
|
||||||
display: "swap",
|
display: "swap",
|
||||||
variable: "--font-america",
|
variable: "--font-america",
|
||||||
|
|||||||
BIN
public/images/companion-preview-dark.webp
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
public/images/companion-preview-light.webp
Normal file
|
After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 501 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 64 KiB |
BIN
public/images/openmonetis_pwa.webp
Normal file
|
After Width: | Height: | Size: 382 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 605 KiB After Width: | Height: | Size: 189 KiB |
BIN
public/images/preview-orcamentos-dark.webp
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
public/images/preview-orcamentos-light.webp
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
public/images/preview-parcelas-dark.webp
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
public/images/preview-parcelas-light.webp
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
public/images/preview-pre-lancamentos-dark.webp
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
public/images/preview-pre-lancamentos-light.webp
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
public/images/pwa-preview-dark.webp
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
public/images/pwa-preview-light.webp
Normal file
|
After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,252 +1,38 @@
|
|||||||
import {
|
import {
|
||||||
RiBankCard2Line,
|
RiAndroidLine,
|
||||||
RiBarChartBoxLine,
|
|
||||||
RiCalendarLine,
|
|
||||||
RiCheckLine,
|
|
||||||
RiCodeSSlashLine,
|
|
||||||
RiDatabase2Line,
|
|
||||||
RiDeviceLine,
|
|
||||||
RiDownloadCloudLine,
|
|
||||||
RiEyeOffLine,
|
|
||||||
RiFileTextLine,
|
|
||||||
RiFlashlightLine,
|
|
||||||
RiGitBranchLine,
|
|
||||||
RiGithubFill,
|
RiGithubFill,
|
||||||
RiInformationLine,
|
|
||||||
RiLayoutGridLine,
|
|
||||||
RiLineChartLine,
|
|
||||||
RiLockLine,
|
|
||||||
RiNotification3Line,
|
|
||||||
RiPercentLine,
|
|
||||||
RiPieChartLine,
|
|
||||||
RiRobot2Line,
|
|
||||||
RiShieldCheckLine,
|
RiShieldCheckLine,
|
||||||
RiSmartphoneLine,
|
RiSmartphoneLine,
|
||||||
RiStarLine,
|
|
||||||
RiTeamLine,
|
|
||||||
RiTimeLine,
|
|
||||||
RiWalletLine,
|
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { ComponentType } from "react";
|
|
||||||
import { AnimateOnScroll } from "@/features/landing/components/animate-on-scroll";
|
import { AnimateOnScroll } from "@/features/landing/components/animate-on-scroll";
|
||||||
import { MobileNav } from "@/features/landing/components/mobile-nav";
|
import { MobileNav } from "@/features/landing/components/mobile-nav";
|
||||||
|
import { ScreenshotTabs } from "@/features/landing/components/screenshot-tabs";
|
||||||
import { SetupTabs } from "@/features/landing/components/setup-tabs";
|
import { SetupTabs } from "@/features/landing/components/setup-tabs";
|
||||||
|
import {
|
||||||
|
companionBanks,
|
||||||
|
companionSteps,
|
||||||
|
extraFeatures,
|
||||||
|
getMetricsItems,
|
||||||
|
mainFeatures,
|
||||||
|
navbarActionClassName,
|
||||||
|
navLinks,
|
||||||
|
pwaHighlights,
|
||||||
|
stackItems,
|
||||||
|
whoIsItForItems,
|
||||||
|
} from "@/features/landing/constants";
|
||||||
|
import { landingImages } from "@/features/landing/images";
|
||||||
|
import { fetchGitHubStats } from "@/features/landing/queries";
|
||||||
import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler";
|
import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler";
|
||||||
import { Logo } from "@/shared/components/logo";
|
import { Logo } from "@/shared/components/logo";
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
AlertDescription,
|
|
||||||
AlertTitle,
|
|
||||||
} from "@/shared/components/ui/alert";
|
|
||||||
import { Badge } from "@/shared/components/ui/badge";
|
import { Badge } from "@/shared/components/ui/badge";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import { Card, CardContent } from "@/shared/components/ui/card";
|
import { Card, CardContent } from "@/shared/components/ui/card";
|
||||||
import { DotPattern } from "@/shared/components/ui/dot-pattern";
|
import { DotPattern } from "@/shared/components/ui/dot-pattern";
|
||||||
import { getOptionalUserSession } from "@/shared/lib/auth/server";
|
import { getOptionalUserSession } from "@/shared/lib/auth/server";
|
||||||
|
|
||||||
async function fetchGitHubStats() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
"https://api.github.com/repos/felipegcoutinho/openmonetis",
|
|
||||||
{ next: { revalidate: 3600 } },
|
|
||||||
);
|
|
||||||
if (!res.ok) return { stars: 200, forks: 60 };
|
|
||||||
const data = await res.json();
|
|
||||||
return {
|
|
||||||
stars: data.stargazers_count as number,
|
|
||||||
forks: data.forks_count as number,
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return { stars: 200, forks: 60 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const navbarActionClassName =
|
|
||||||
"border-black/10 bg-transparent text-black/75 shadow-none hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-black/20 data-[state=open]:bg-black/10 data-[state=open]:text-black";
|
|
||||||
|
|
||||||
type FeatureItem = {
|
|
||||||
icon: ComponentType<{ className?: string; style?: React.CSSProperties }>;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
colorVar: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mainFeatures: FeatureItem[] = [
|
|
||||||
{
|
|
||||||
icon: RiWalletLine,
|
|
||||||
title: "Contas e transações",
|
|
||||||
description:
|
|
||||||
"Registre suas contas bancárias, cartões e dinheiro. Adicione receitas, despesas e transferências. Organize por categorias. Extratos detalhados por conta.",
|
|
||||||
colorVar: "var(--data-9)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiPercentLine,
|
|
||||||
title: "Parcelamentos avançados",
|
|
||||||
description:
|
|
||||||
"Controle completo de compras parceladas. Antecipe parcelas com cálculo automático de desconto. Veja análise consolidada de todas as parcelas em aberto.",
|
|
||||||
colorVar: "var(--data-4)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiRobot2Line,
|
|
||||||
title: "Insights com IA",
|
|
||||||
description:
|
|
||||||
"Análises financeiras geradas por IA (Claude, GPT, Gemini). Insights personalizados sobre seus padrões de gastos e recomendações inteligentes.",
|
|
||||||
colorVar: "var(--data-8)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiBarChartBoxLine,
|
|
||||||
title: "Relatórios e gráficos",
|
|
||||||
description:
|
|
||||||
"Dashboard com 20+ widgets interativos. Relatórios detalhados por categoria. Gráficos de evolução e comparativos. Exportação em PDF e Excel.",
|
|
||||||
colorVar: "var(--data-5)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiBankCard2Line,
|
|
||||||
title: "Faturas de cartão",
|
|
||||||
description:
|
|
||||||
"Cadastre seus cartões e acompanhe as faturas por período. Veja o que ainda não foi fechado. Controle limites, vencimentos e fechamentos.",
|
|
||||||
colorVar: "var(--data-1)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiTeamLine,
|
|
||||||
title: "Gestão colaborativa",
|
|
||||||
description:
|
|
||||||
"Compartilhe pagadores com permissões granulares (admin/viewer). Notificações automáticas por e-mail. Colabore em lançamentos compartilhados.",
|
|
||||||
colorVar: "var(--data-3)",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const extraFeatures: FeatureItem[] = [
|
|
||||||
{
|
|
||||||
icon: RiPieChartLine,
|
|
||||||
title: "Categorias e orçamentos",
|
|
||||||
description:
|
|
||||||
"Crie categorias personalizadas e defina orçamentos mensais com indicadores visuais.",
|
|
||||||
colorVar: "var(--data-7)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiFileTextLine,
|
|
||||||
title: "Anotações e tarefas",
|
|
||||||
description:
|
|
||||||
"Notas de texto e listas de tarefas com checkboxes. Arquivamento para manter histórico.",
|
|
||||||
colorVar: "var(--data-6)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiCalendarLine,
|
|
||||||
title: "Calendário financeiro",
|
|
||||||
description:
|
|
||||||
"Visualize transações em calendário mensal. Nunca perca prazos de pagamentos.",
|
|
||||||
colorVar: "var(--data-2)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiDownloadCloudLine,
|
|
||||||
title: "Importação em massa",
|
|
||||||
description: "Lance múltiplos lançamentos de uma vez",
|
|
||||||
colorVar: "var(--data-9)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiEyeOffLine,
|
|
||||||
title: "Modo privacidade",
|
|
||||||
description:
|
|
||||||
"Oculte valores sensíveis com um clique. Tema dark/light. Calculadora integrada.",
|
|
||||||
colorVar: "var(--data-4)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiFlashlightLine,
|
|
||||||
title: "Performance otimizada",
|
|
||||||
description: "Sistema rápido e com alta performance",
|
|
||||||
colorVar: "var(--data-5)",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const screenshotSections = [
|
|
||||||
{
|
|
||||||
title: "Lançamentos",
|
|
||||||
description: "Registre e organize todas as suas transações financeiras",
|
|
||||||
lightSrc: "/images/preview-lancamentos-light.webp",
|
|
||||||
darkSrc: "/images/preview-lancamentos-dark.webp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Calendário",
|
|
||||||
description: "Visualize suas finanças no calendário mensal",
|
|
||||||
lightSrc: "/images/preview-calendario-light.webp",
|
|
||||||
darkSrc: "/images/preview-calendario-dark.webp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Cartões",
|
|
||||||
description: "Acompanhe faturas, limites e vencimentos dos seus cartões",
|
|
||||||
lightSrc: "/images/preview-cartao-light.webp",
|
|
||||||
darkSrc: "/images/preview-cartao-dark.webp",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const companionBanks = [
|
|
||||||
{ name: "Nubank", logo: "/logos/nubank.png" },
|
|
||||||
{ name: "Itaú", logo: "/logos/itau.png" },
|
|
||||||
{ name: "Inter", logo: "/logos/interpj.png" },
|
|
||||||
{ name: "Mercado Pago", logo: "/logos/mercadopago.png" },
|
|
||||||
{ name: "Outros", logo: null },
|
|
||||||
];
|
|
||||||
|
|
||||||
const stackItems = [
|
|
||||||
{
|
|
||||||
icon: RiCodeSSlashLine,
|
|
||||||
title: "Frontend",
|
|
||||||
subtitle: "Next.js 16, TypeScript, Tailwind CSS, shadcn/ui",
|
|
||||||
description: "Interface moderna e responsiva com React 19 e App Router",
|
|
||||||
colorVar: "var(--data-3)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiDatabase2Line,
|
|
||||||
title: "Backend",
|
|
||||||
subtitle: "PostgreSQL 18, Drizzle ORM, Better Auth",
|
|
||||||
description: "Banco relacional robusto com type-safe ORM",
|
|
||||||
colorVar: "var(--data-9)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiShieldCheckLine,
|
|
||||||
title: "Segurança",
|
|
||||||
subtitle: "Better Auth com OAuth (Google) e autenticação por email",
|
|
||||||
description: "Sessões seguras e proteção de rotas por middleware",
|
|
||||||
colorVar: "var(--data-1)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiDeviceLine,
|
|
||||||
title: "Deploy",
|
|
||||||
subtitle:
|
|
||||||
"Docker com multi-stage build, health checks e volumes persistentes",
|
|
||||||
description: "Fácil de rodar localmente ou em qualquer servidor",
|
|
||||||
colorVar: "var(--data-5)",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const whoIsItForItems = [
|
|
||||||
{
|
|
||||||
icon: RiTimeLine,
|
|
||||||
title: "Tem disciplina de registrar gastos",
|
|
||||||
description:
|
|
||||||
"Não se importa em dedicar alguns minutos por dia ou semana para manter tudo atualizado",
|
|
||||||
colorVar: "var(--data-4)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiLockLine,
|
|
||||||
title: "Quer controle total sobre seus dados",
|
|
||||||
description:
|
|
||||||
"Prefere hospedar seus próprios dados ao invés de depender de serviços terceiros",
|
|
||||||
colorVar: "var(--data-9)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiLineChartLine,
|
|
||||||
title: "Gosta de entender exatamente onde o dinheiro vai",
|
|
||||||
description:
|
|
||||||
"Quer visualizar padrões de gastos e tomar decisões informadas",
|
|
||||||
colorVar: "var(--data-3)",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const [session, headersList, githubStats] = await Promise.all([
|
const [session, headersList, githubStats] = await Promise.all([
|
||||||
getOptionalUserSession(),
|
getOptionalUserSession(),
|
||||||
@@ -259,6 +45,7 @@ export default async function Page() {
|
|||||||
"",
|
"",
|
||||||
).replace(/:\d+$/, "");
|
).replace(/:\d+$/, "");
|
||||||
const isPublicDomain = !!(publicDomain && hostname === publicDomain);
|
const isPublicDomain = !!(publicDomain && hostname === publicDomain);
|
||||||
|
const metricsItems = getMetricsItems(githubStats.stars, githubStats.forks);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col">
|
<div className="flex min-h-screen flex-col">
|
||||||
@@ -273,13 +60,7 @@ export default async function Page() {
|
|||||||
|
|
||||||
{/* Center Navigation Links */}
|
{/* Center Navigation Links */}
|
||||||
<nav className="hidden md:flex items-center gap-1 absolute left-1/2 -translate-x-1/2">
|
<nav className="hidden md:flex items-center gap-1 absolute left-1/2 -translate-x-1/2">
|
||||||
{[
|
{navLinks.map(({ href, label }) => (
|
||||||
{ href: "#telas", label: "conheça as telas" },
|
|
||||||
{ href: "#funcionalidades", label: "funcionalidades" },
|
|
||||||
{ href: "#companion", label: "companion" },
|
|
||||||
{ href: "#stack", label: "stack" },
|
|
||||||
{ href: "#como-usar", label: "como usar" },
|
|
||||||
].map(({ href, label }) => (
|
|
||||||
<a
|
<a
|
||||||
key={href}
|
key={href}
|
||||||
href={href}
|
href={href}
|
||||||
@@ -333,9 +114,8 @@ export default async function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Hero Section — texto + preview integrado */}
|
{/* Hero Section */}
|
||||||
<section className="relative overflow-hidden pt-14 md:pt-20 lg:pt-24 pb-0">
|
<section className="relative overflow-hidden pt-14 md:pt-20 lg:pt-24 pb-0">
|
||||||
{/* Background — DotPattern fade conectando com navbar */}
|
|
||||||
<div className="pointer-events-none absolute inset-x-0 top-0 h-80 overflow-hidden">
|
<div className="pointer-events-none absolute inset-x-0 top-0 h-80 overflow-hidden">
|
||||||
<DotPattern
|
<DotPattern
|
||||||
width={20}
|
width={20}
|
||||||
@@ -349,7 +129,6 @@ export default async function Page() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-8xl mx-auto px-4 relative">
|
<div className="max-w-8xl mx-auto px-4 relative">
|
||||||
{/* Texto */}
|
|
||||||
<div className="mx-auto flex max-w-3xl flex-col items-center text-center gap-5 md:gap-6 pb-10 md:pb-14">
|
<div className="mx-auto flex max-w-3xl flex-col items-center text-center gap-5 md:gap-6 pb-10 md:pb-14">
|
||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
<RiGithubFill className="size-4 mr-1" />
|
<RiGithubFill className="size-4 mr-1" />
|
||||||
@@ -397,7 +176,6 @@ export default async function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dashboard preview integrado ao hero */}
|
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<div className="rounded-t-xl overflow-hidden border-x border-t bg-card">
|
<div className="rounded-t-xl overflow-hidden border-x border-t bg-card">
|
||||||
<div className="flex items-center gap-1.5 px-3 h-8 border-b bg-muted/50">
|
<div className="flex items-center gap-1.5 px-3 h-8 border-b bg-muted/50">
|
||||||
@@ -407,7 +185,7 @@ export default async function Page() {
|
|||||||
<div className="ml-2 flex-1 max-w-52 h-4 rounded bg-muted-foreground/10" />
|
<div className="ml-2 flex-1 max-w-52 h-4 rounded bg-muted-foreground/10" />
|
||||||
</div>
|
</div>
|
||||||
<Image
|
<Image
|
||||||
src="/images/dashboard-preview-light.webp"
|
src={landingImages.hero.light}
|
||||||
alt="openmonetis Dashboard Preview"
|
alt="openmonetis Dashboard Preview"
|
||||||
width={1920}
|
width={1920}
|
||||||
height={1080}
|
height={1080}
|
||||||
@@ -415,7 +193,7 @@ export default async function Page() {
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<Image
|
<Image
|
||||||
src="/images/dashboard-preview-dark.webp"
|
src={landingImages.hero.dark}
|
||||||
alt="openmonetis Dashboard Preview"
|
alt="openmonetis Dashboard Preview"
|
||||||
width={1920}
|
width={1920}
|
||||||
height={1080}
|
height={1080}
|
||||||
@@ -432,32 +210,7 @@ export default async function Page() {
|
|||||||
<div className="max-w-8xl mx-auto px-4">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-4xl">
|
<div className="mx-auto max-w-4xl">
|
||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4 md:gap-8">
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-4 md:gap-8">
|
||||||
{[
|
{metricsItems.map(({ icon: Icon, value, label, colorVar }) => (
|
||||||
{
|
|
||||||
icon: RiLayoutGridLine,
|
|
||||||
value: "20+",
|
|
||||||
label: "Widgets no dashboard",
|
|
||||||
colorVar: "var(--data-9)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiShieldCheckLine,
|
|
||||||
value: "100%",
|
|
||||||
label: "Self-hosted",
|
|
||||||
colorVar: "var(--data-1)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiStarLine,
|
|
||||||
value: `${githubStats.stars}`,
|
|
||||||
label: "Stars no GitHub",
|
|
||||||
colorVar: "var(--data-4)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RiGitBranchLine,
|
|
||||||
value: `${githubStats.forks}`,
|
|
||||||
label: "Forks no GitHub",
|
|
||||||
colorVar: "var(--data-3)",
|
|
||||||
},
|
|
||||||
].map(({ icon: Icon, value, label, colorVar }) => (
|
|
||||||
<div
|
<div
|
||||||
key={label}
|
key={label}
|
||||||
className="flex flex-col items-center text-center gap-1.5"
|
className="flex flex-col items-center text-center gap-1.5"
|
||||||
@@ -494,50 +247,17 @@ export default async function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</AnimateOnScroll>
|
</AnimateOnScroll>
|
||||||
|
|
||||||
<div className="space-y-10 md:space-y-14">
|
<AnimateOnScroll>
|
||||||
{screenshotSections.map((section) => (
|
<ScreenshotTabs />
|
||||||
<AnimateOnScroll key={section.title}>
|
|
||||||
<div className="mb-3 text-center">
|
|
||||||
<h3 className="font-semibold text-base md:text-lg">
|
|
||||||
{section.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{section.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg overflow-hidden border bg-card">
|
|
||||||
<div className="flex items-center gap-1.5 px-3 h-8 border-b bg-muted/50">
|
|
||||||
<div className="size-2.5 rounded-full bg-muted-foreground/20" />
|
|
||||||
<div className="size-2.5 rounded-full bg-muted-foreground/20" />
|
|
||||||
<div className="size-2.5 rounded-full bg-muted-foreground/20" />
|
|
||||||
<div className="ml-2 flex-1 max-w-52 h-4 rounded bg-muted-foreground/10" />
|
|
||||||
</div>
|
|
||||||
<Image
|
|
||||||
src={section.lightSrc}
|
|
||||||
alt={`Preview ${section.title}`}
|
|
||||||
width={1920}
|
|
||||||
height={1080}
|
|
||||||
className="w-full h-auto dark:hidden"
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
src={section.darkSrc}
|
|
||||||
alt={`Preview ${section.title}`}
|
|
||||||
width={1920}
|
|
||||||
height={1080}
|
|
||||||
className="w-full h-auto hidden dark:block"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AnimateOnScroll>
|
</AnimateOnScroll>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Features Section */}
|
{/* Features Section */}
|
||||||
<section id="funcionalidades" className="py-12 md:py-24">
|
<section id="funcionalidades" className="py-12 md:py-24 bg-muted/40">
|
||||||
<div className="max-w-8xl mx-auto px-4">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-5xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="text-center mb-8 md:mb-12">
|
<div className="text-center mb-8 md:mb-12">
|
||||||
<Badge variant="outline" className="mb-4">
|
<Badge variant="outline" className="mb-4">
|
||||||
@@ -553,7 +273,6 @@ export default async function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</AnimateOnScroll>
|
</AnimateOnScroll>
|
||||||
|
|
||||||
{/* Main Features - larger cards */}
|
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="grid gap-4 md:gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{mainFeatures.map((feature) => (
|
{mainFeatures.map((feature) => (
|
||||||
@@ -586,11 +305,10 @@ export default async function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</AnimateOnScroll>
|
</AnimateOnScroll>
|
||||||
|
|
||||||
{/* Extra Features - compact list */}
|
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="mt-8 md:mt-12">
|
<div className="mt-8 md:mt-12">
|
||||||
<h3 className="text-lg font-semibold text-center mb-4 md:mb-6 text-muted-foreground">
|
<h3 className="text-sm font-medium text-center mb-4 md:mb-6 text-muted-foreground">
|
||||||
E mais...
|
Também inclui
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{extraFeatures.map((feature) => (
|
{extraFeatures.map((feature) => (
|
||||||
@@ -626,85 +344,141 @@ export default async function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Companion Section */}
|
{/* Mobile Section */}
|
||||||
<section id="companion" className="py-12 md:py-24">
|
<section id="mobile" className="py-12 md:py-24">
|
||||||
<div className="max-w-8xl mx-auto px-4">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-5xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
|
{/* Header */}
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="grid gap-8 md:gap-12 md:grid-cols-2 items-center">
|
<div className="text-center mb-12 md:mb-20">
|
||||||
{/* Text content */}
|
|
||||||
<div className="order-2 md:order-1">
|
|
||||||
<Badge variant="outline" className="mb-4">
|
<Badge variant="outline" className="mb-4">
|
||||||
<RiSmartphoneLine className="size-3.5 mr-1" />
|
<RiSmartphoneLine className="size-3.5 mr-1" />
|
||||||
App Android
|
Mobile
|
||||||
</Badge>
|
</Badge>
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight mb-3 md:mb-4">
|
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight mb-3 md:mb-4">
|
||||||
Capture automaticamente do seu celular
|
Use o OpenMonetis no celular sem perder o fluxo
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-base md:text-lg text-muted-foreground mb-6">
|
<p className="text-base md:text-lg text-muted-foreground max-w-2xl mx-auto px-4 sm:px-0">
|
||||||
O OpenMonetis Companion captura notificações de apps
|
Instale como PWA para acesso rápido no dia a dia. No Android,
|
||||||
bancários e cria pré-lançamentos automaticamente para você
|
use o Companion para capturar notificações bancárias
|
||||||
revisar.
|
automaticamente.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
</AnimateOnScroll>
|
||||||
|
|
||||||
{/* Flow steps */}
|
{/* PWA — imagem esquerda, texto direita */}
|
||||||
<div className="space-y-3 mb-6">
|
<AnimateOnScroll>
|
||||||
{[
|
<div className="grid gap-10 lg:gap-16 lg:grid-cols-2 items-center mb-16 md:mb-24">
|
||||||
{
|
<div className="flex justify-center">
|
||||||
icon: RiNotification3Line,
|
<div className="relative">
|
||||||
title: "Notificação bancária chega",
|
<div className="absolute inset-0 bg-primary/8 rounded-3xl blur-3xl scale-90" />
|
||||||
subtitle: "O Companion intercepta automaticamente",
|
<Image
|
||||||
colorVar: "var(--data-1)",
|
src={landingImages.pwa.light}
|
||||||
},
|
alt="Preview PWA"
|
||||||
{
|
width={390}
|
||||||
icon: RiSmartphoneLine,
|
height={844}
|
||||||
title: "Dados extraídos e enviados",
|
className="relative h-auto w-56 md:w-64 rounded-3xl shadow-lg dark:hidden"
|
||||||
subtitle: "Valor, descrição e banco são identificados",
|
/>
|
||||||
colorVar: "var(--data-4)",
|
<Image
|
||||||
},
|
src={landingImages.pwa.dark}
|
||||||
{
|
alt="Preview PWA"
|
||||||
icon: RiCheckLine,
|
width={390}
|
||||||
title: "Revise e confirme no OpenMonetis",
|
height={844}
|
||||||
subtitle:
|
className="relative h-auto w-56 md:w-64 rounded-3xl shadow-lg hidden dark:block"
|
||||||
"Pré-lançamentos ficam na inbox para sua aprovação",
|
|
||||||
colorVar: "var(--data-9)",
|
|
||||||
},
|
|
||||||
].map((step) => (
|
|
||||||
<div key={step.title} className="flex items-start gap-3">
|
|
||||||
<div
|
|
||||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `color-mix(in oklch, ${step.colorVar} 14%, transparent)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<step.icon
|
|
||||||
className="size-4"
|
|
||||||
style={{ color: step.colorVar }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">{step.title}</p>
|
<Badge variant="outline" className="mb-4">
|
||||||
<p className="text-xs text-muted-foreground">
|
<RiSmartphoneLine className="size-3.5 mr-1" />
|
||||||
{step.subtitle}
|
PWA instalável
|
||||||
|
</Badge>
|
||||||
|
<h3 className="text-2xl md:text-3xl font-bold tracking-tight mb-3">
|
||||||
|
Leve o OpenMonetis para a tela inicial
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mb-6 leading-relaxed">
|
||||||
|
Adicione à tela inicial e abra direto, como um app. Sem
|
||||||
|
depender de uma aba perdida no navegador. Funciona em
|
||||||
|
Android, iOS e desktop.
|
||||||
</p>
|
</p>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{pwaHighlights.map((item) => (
|
||||||
|
<li key={item.title} className="flex items-start gap-3">
|
||||||
|
<div
|
||||||
|
className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-md"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `color-mix(in oklch, ${item.colorVar} 14%, transparent)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<item.icon
|
||||||
|
className="size-[15px]"
|
||||||
|
style={{ color: item.colorVar }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p className="text-sm">
|
||||||
|
<span className="font-medium">{item.title}</span>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{" "}
|
||||||
|
— {item.description}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimateOnScroll>
|
||||||
|
|
||||||
{/* Supported banks */}
|
{/* Companion — texto esquerda, imagem direita */}
|
||||||
<div className="mb-6">
|
<AnimateOnScroll>
|
||||||
<p className="text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wider">
|
<div className="grid gap-10 lg:gap-16 lg:grid-cols-2 items-center border-t pt-16 md:pt-24">
|
||||||
Bancos suportados
|
<div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<Badge variant="outline">
|
||||||
|
<RiAndroidLine className="size-3.5 mr-1" />
|
||||||
|
Companion Android
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl md:text-3xl font-bold tracking-tight mb-3">
|
||||||
|
Capture, envie e revise no mesmo fluxo
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mb-6 leading-relaxed">
|
||||||
|
O Companion captura notificações de apps bancários e cria
|
||||||
|
pré-lançamentos automaticamente para você revisar na inbox.
|
||||||
|
</p>
|
||||||
|
<ol className="space-y-3 mb-6">
|
||||||
|
{companionSteps.map((step, index) => (
|
||||||
|
<li key={step.title} className="flex items-start gap-3">
|
||||||
|
<span
|
||||||
|
className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs font-bold"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `color-mix(in oklch, ${step.colorVar} 14%, transparent)`,
|
||||||
|
color: step.colorVar,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
<p className="text-sm">
|
||||||
|
<span className="font-medium">{step.title}</span>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{" "}
|
||||||
|
— {step.description}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
|
||||||
|
Bancos testados
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{companionBanks.map((bank) => (
|
{companionBanks.map((bank) => (
|
||||||
<span
|
<span
|
||||||
key={bank.name}
|
key={bank.name}
|
||||||
className="inline-flex items-center gap-1.5 rounded-full border bg-muted/50 py-1 text-xs font-medium"
|
className="inline-flex items-center gap-1.5 rounded-full border py-1 pr-3 text-xs font-medium"
|
||||||
style={{
|
style={{
|
||||||
paddingLeft: bank.logo ? "4px" : "12px",
|
paddingLeft: bank.logo ? "4px" : "12px",
|
||||||
paddingRight: "12px",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{bank.logo && (
|
{bank.logo && (
|
||||||
@@ -720,28 +494,32 @@ export default async function Page() {
|
|||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="https://github.com/felipegcoutinho/openmonetis-companion"
|
href="https://github.com/felipegcoutinho/openmonetis-companion"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
className="mt-4 inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
<Button variant="outline" className="gap-2">
|
<RiGithubFill className="size-3.5" />
|
||||||
<RiGithubFill className="size-4" />
|
|
||||||
Ver no GitHub
|
Ver no GitHub
|
||||||
</Button>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Companion Screenshot */}
|
<div className="flex justify-center order-first lg:order-last">
|
||||||
<div className="order-1 md:order-2 flex items-center justify-center">
|
<div className="relative">
|
||||||
<div className="w-full max-w-[220px] md:max-w-[260px]">
|
<div className="absolute inset-0 bg-primary/8 rounded-3xl blur-3xl scale-90" />
|
||||||
<Image
|
<Image
|
||||||
src="/images/openmonetis_companion.webp"
|
src={landingImages.companion.light}
|
||||||
alt="OpenMonetis Companion App"
|
alt="Preview Companion"
|
||||||
width={1080}
|
width={390}
|
||||||
height={2217}
|
height={844}
|
||||||
className="w-full h-auto rounded-2xl"
|
className="relative h-auto w-56 md:w-64 rounded-3xl shadow-lg dark:hidden"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src={landingImages.companion.dark}
|
||||||
|
alt="Preview Companion"
|
||||||
|
width={390}
|
||||||
|
height={844}
|
||||||
|
className="relative h-auto w-56 md:w-64 rounded-3xl shadow-lg hidden dark:block"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -752,19 +530,19 @@ export default async function Page() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Tech Stack Section */}
|
{/* Tech Stack Section */}
|
||||||
<section id="stack" className="py-12 md:py-24">
|
<section id="stack" className="py-12 md:py-24 bg-muted/40">
|
||||||
<div className="max-w-8xl mx-auto px-4">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-5xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="text-center mb-8 md:mb-12">
|
<div className="text-center mb-8 md:mb-12">
|
||||||
<Badge variant="outline" className="mb-4">
|
<Badge variant="outline" className="mb-4">
|
||||||
Stack técnica
|
Stack técnica
|
||||||
</Badge>
|
</Badge>
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight mb-3 md:mb-4">
|
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight mb-3 md:mb-4">
|
||||||
Construído com tecnologias modernas
|
O que roda por baixo
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-base md:text-lg text-muted-foreground max-w-2xl mx-auto px-4 sm:px-0">
|
<p className="text-base md:text-lg text-muted-foreground max-w-2xl mx-auto px-4 sm:px-0">
|
||||||
Open source, self-hosted e fácil de customizar
|
Self-hosted, open source, type-safe do banco ao frontend
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</AnimateOnScroll>
|
</AnimateOnScroll>
|
||||||
@@ -793,9 +571,6 @@ export default async function Page() {
|
|||||||
<p className="text-sm text-muted-foreground mb-2 md:mb-3">
|
<p className="text-sm text-muted-foreground mb-2 md:mb-3">
|
||||||
{item.subtitle}
|
{item.subtitle}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{item.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -803,13 +578,6 @@ export default async function Page() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</AnimateOnScroll>
|
</AnimateOnScroll>
|
||||||
|
|
||||||
<div className="mt-6 md:mt-8 text-center">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Seus dados ficam no seu controle. Pode rodar localmente ou no
|
|
||||||
seu próprio servidor.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -817,7 +585,7 @@ export default async function Page() {
|
|||||||
{/* How to run Section */}
|
{/* How to run Section */}
|
||||||
<section id="como-usar" className="py-12 md:py-24">
|
<section id="como-usar" className="py-12 md:py-24">
|
||||||
<div className="max-w-8xl mx-auto px-4">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-3xl">
|
<div className="mx-auto max-w-4xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="text-center mb-8 md:mb-12">
|
<div className="text-center mb-8 md:mb-12">
|
||||||
<Badge variant="outline" className="mb-4">
|
<Badge variant="outline" className="mb-4">
|
||||||
@@ -850,19 +618,19 @@ export default async function Page() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Who is this for Section */}
|
{/* Who is this for Section */}
|
||||||
<section className="py-12 md:py-24">
|
<section id="para-quem-e" className="py-12 md:py-24 bg-muted/40">
|
||||||
<div className="max-w-8xl mx-auto px-4">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<div className="mx-auto max-w-3xl">
|
<div className="mx-auto max-w-4xl">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="text-center mb-8 md:mb-12">
|
<div className="text-center mb-8 md:mb-12">
|
||||||
<Badge variant="outline" className="mb-4">
|
<Badge variant="outline" className="mb-4">
|
||||||
Para quem é?
|
Para quem é?
|
||||||
</Badge>
|
</Badge>
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight mb-3 md:mb-4">
|
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight mb-3 md:mb-4">
|
||||||
Para quem funciona?
|
Feito para quem gosta de controle
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-base md:text-lg text-muted-foreground px-4 sm:px-0">
|
<p className="text-base md:text-lg text-muted-foreground px-4 sm:px-0">
|
||||||
O openmonetis funciona melhor se você:
|
O OpenMonetis não é para todo mundo.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</AnimateOnScroll>
|
</AnimateOnScroll>
|
||||||
@@ -896,19 +664,6 @@ export default async function Page() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</AnimateOnScroll>
|
</AnimateOnScroll>
|
||||||
|
|
||||||
<AnimateOnScroll>
|
|
||||||
<Alert className="mt-6 md:mt-8">
|
|
||||||
<RiInformationLine />
|
|
||||||
<AlertTitle>Não é para todo mundo</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
Se você não se encaixa nisso, provavelmente vai abandonar
|
|
||||||
depois de uma semana. Tudo certo! Existem outras ferramentas
|
|
||||||
com sincronização automática que podem funcionar melhor pra
|
|
||||||
você.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</AnimateOnScroll>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -917,7 +672,7 @@ export default async function Page() {
|
|||||||
<section className="py-12 md:py-24">
|
<section className="py-12 md:py-24">
|
||||||
<div className="max-w-8xl mx-auto px-4">
|
<div className="max-w-8xl mx-auto px-4">
|
||||||
<AnimateOnScroll>
|
<AnimateOnScroll>
|
||||||
<div className="mx-auto max-w-3xl rounded-2xl border bg-card px-6 py-12 md:py-16 text-center">
|
<div className="mx-auto max-w-4xl rounded-2xl border bg-card px-8 py-12 md:py-16 text-center">
|
||||||
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight mb-3 md:mb-4">
|
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight mb-3 md:mb-4">
|
||||||
Pronto para testar?
|
Pronto para testar?
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -51,7 +51,6 @@
|
|||||||
--chart-9: var(--color-cyan-500);
|
--chart-9: var(--color-cyan-500);
|
||||||
--chart-10: var(--color-lime-500);
|
--chart-10: var(--color-lime-500);
|
||||||
|
|
||||||
/* Data palette — análoga quente (hue 0–120), família do primary */
|
|
||||||
--data-1: oklch(58% 0.22 18); /* vermelho-tijolo */
|
--data-1: oklch(58% 0.22 18); /* vermelho-tijolo */
|
||||||
--data-2: oklch(64% 0.22 30); /* vermelho-laranja */
|
--data-2: oklch(64% 0.22 30); /* vermelho-laranja */
|
||||||
--data-3: oklch(69% 0.21 42); /* laranja (≈ primary) */
|
--data-3: oklch(69% 0.21 42); /* laranja (≈ primary) */
|
||||||
@@ -93,23 +92,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(22% 0.004 55);
|
--background: oklch(18% 0.004 55);
|
||||||
--foreground: oklch(93% 0.008 80);
|
--foreground: oklch(93% 0.008 80);
|
||||||
--card: oklch(25.5% 0.004 55);
|
--card: oklch(21.5% 0.004 55);
|
||||||
--card-foreground: var(--foreground);
|
--card-foreground: var(--foreground);
|
||||||
--popover: oklch(28% 0.004 55);
|
--popover: oklch(24% 0.004 55);
|
||||||
--popover-foreground: var(--foreground);
|
--popover-foreground: var(--foreground);
|
||||||
|
|
||||||
--primary: oklch(72.069% 0.18335 44.069);
|
--primary: oklch(72.069% 0.18335 44.069);
|
||||||
--primary-foreground: oklch(16% 0.004 60);
|
--primary-foreground: oklch(16% 0.004 60);
|
||||||
|
|
||||||
--secondary: oklch(29% 0.004 55);
|
--secondary: oklch(25% 0.004 55);
|
||||||
--secondary-foreground: var(--foreground);
|
--secondary-foreground: var(--foreground);
|
||||||
|
|
||||||
--muted: oklch(32% 0.0035 55);
|
--muted: oklch(28% 0.0035 55);
|
||||||
--muted-foreground: oklch(73% 0.006 75);
|
--muted-foreground: oklch(73% 0.006 75);
|
||||||
|
|
||||||
--accent: oklch(33% 0.005 55);
|
--accent: oklch(29% 0.005 55);
|
||||||
--accent-foreground: var(--foreground);
|
--accent-foreground: var(--foreground);
|
||||||
|
|
||||||
--success: oklch(62% 0.16 150);
|
--success: oklch(62% 0.16 150);
|
||||||
@@ -121,7 +120,7 @@
|
|||||||
--destructive: oklch(62% 0.2 28);
|
--destructive: oklch(62% 0.2 28);
|
||||||
--destructive-foreground: oklch(98% 0.005 30);
|
--destructive-foreground: oklch(98% 0.005 30);
|
||||||
|
|
||||||
--border: oklch(35% 0.004 55);
|
--border: oklch(31% 0.004 55);
|
||||||
--input: var(--border);
|
--input: var(--border);
|
||||||
--ring: var(--primary);
|
--ring: var(--primary);
|
||||||
|
|
||||||
@@ -136,8 +135,6 @@
|
|||||||
--chart-9: var(--color-cyan-500);
|
--chart-9: var(--color-cyan-500);
|
||||||
--chart-10: var(--color-lime-500);
|
--chart-10: var(--color-lime-500);
|
||||||
|
|
||||||
/* Data palette — dark mode (ligeiramente mais vivos) */
|
|
||||||
/* Data palette dark — ligeiramente mais vivos para contrastar no fundo escuro */
|
|
||||||
--data-1: oklch(66% 0.22 18);
|
--data-1: oklch(66% 0.22 18);
|
||||||
--data-2: oklch(72% 0.22 30);
|
--data-2: oklch(72% 0.22 30);
|
||||||
--data-3: oklch(76% 0.21 42);
|
--data-3: oklch(76% 0.21 42);
|
||||||
@@ -149,13 +146,13 @@
|
|||||||
--data-9: oklch(69% 0.17 120);
|
--data-9: oklch(69% 0.17 120);
|
||||||
--data-10: oklch(63% 0.15 10);
|
--data-10: oklch(63% 0.15 10);
|
||||||
|
|
||||||
--sidebar: oklch(19.5% 0.004 55);
|
--sidebar: oklch(15.5% 0.004 55);
|
||||||
--sidebar-foreground: var(--foreground);
|
--sidebar-foreground: var(--foreground);
|
||||||
--sidebar-primary: var(--primary);
|
--sidebar-primary: var(--primary);
|
||||||
--sidebar-primary-foreground: var(--primary-foreground);
|
--sidebar-primary-foreground: var(--primary-foreground);
|
||||||
--sidebar-accent: oklch(30% 0.004 55);
|
--sidebar-accent: oklch(26% 0.004 55);
|
||||||
--sidebar-accent-foreground: var(--foreground);
|
--sidebar-accent-foreground: var(--foreground);
|
||||||
--sidebar-border: oklch(34% 0.004 55);
|
--sidebar-border: oklch(30% 0.004 55);
|
||||||
--sidebar-ring: var(--primary);
|
--sidebar-ring: var(--primary);
|
||||||
|
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -15,9 +15,10 @@ import {
|
|||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ href: "#telas", label: "Conheça as telas" },
|
{ href: "#telas", label: "Conheça as telas" },
|
||||||
{ href: "#funcionalidades", label: "Funcionalidades" },
|
{ href: "#funcionalidades", label: "Funcionalidades" },
|
||||||
{ href: "#companion", label: "Companion" },
|
{ href: "#mobile", label: "Mobile" },
|
||||||
{ href: "#stack", label: "Stack" },
|
{ href: "#stack", label: "Stack" },
|
||||||
{ href: "#como-usar", label: "Como usar" },
|
{ href: "#como-usar", label: "Como usar" },
|
||||||
|
{ href: "#para-quem-e", label: "Para quem é?" },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface MobileNavProps {
|
interface MobileNavProps {
|
||||||
@@ -26,7 +27,11 @@ interface MobileNavProps {
|
|||||||
triggerClassName?: string;
|
triggerClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MobileNav({ isPublicDomain, isLoggedIn, triggerClassName }: MobileNavProps) {
|
export function MobileNav({
|
||||||
|
isPublicDomain,
|
||||||
|
isLoggedIn,
|
||||||
|
triggerClassName,
|
||||||
|
}: MobileNavProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
107
src/features/landing/components/screenshot-tabs.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
RiArrowLeftRightLine,
|
||||||
|
RiAtLine,
|
||||||
|
RiBankCard2Line,
|
||||||
|
RiBarChart2Line,
|
||||||
|
RiCalendarEventLine,
|
||||||
|
RiSecurePaymentLine,
|
||||||
|
} from "@remixicon/react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { landingImages } from "@/features/landing/images";
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from "@/shared/components/ui/tabs";
|
||||||
|
|
||||||
|
const { screenshots } = landingImages;
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{
|
||||||
|
value: "lancamentos",
|
||||||
|
label: "Lançamentos",
|
||||||
|
icon: RiArrowLeftRightLine,
|
||||||
|
...screenshots.lancamentos,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "pre-lancamentos",
|
||||||
|
label: "Pré-lançamentos",
|
||||||
|
icon: RiAtLine,
|
||||||
|
...screenshots.preLancamentos,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "orcamentos",
|
||||||
|
label: "Orçamentos",
|
||||||
|
icon: RiBarChart2Line,
|
||||||
|
...screenshots.orcamentos,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "parcelas",
|
||||||
|
label: "Análise de Parcelas",
|
||||||
|
icon: RiSecurePaymentLine,
|
||||||
|
...screenshots.parcelas,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "calendario",
|
||||||
|
label: "Calendário",
|
||||||
|
icon: RiCalendarEventLine,
|
||||||
|
...screenshots.calendario,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "cartoes",
|
||||||
|
label: "Cartões",
|
||||||
|
icon: RiBankCard2Line,
|
||||||
|
...screenshots.cartoes,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ScreenshotTabs() {
|
||||||
|
return (
|
||||||
|
<Tabs defaultValue="lancamentos" className="w-full">
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<TabsList className="w-full h-auto flex-wrap gap-1">
|
||||||
|
{sections.map((s) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={s.value}
|
||||||
|
value={s.value}
|
||||||
|
className="flex-1 gap-1.5 lowercase"
|
||||||
|
>
|
||||||
|
<s.icon className="size-4" />
|
||||||
|
{s.label}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{sections.map((s) => (
|
||||||
|
<TabsContent key={s.value} value={s.value} className="w-full mt-0">
|
||||||
|
<div className="rounded-lg overflow-hidden border bg-card">
|
||||||
|
<div className="flex items-center gap-1.5 px-3 h-8 border-b bg-muted/50">
|
||||||
|
<div className="size-2.5 rounded-full bg-muted-foreground/20" />
|
||||||
|
<div className="size-2.5 rounded-full bg-muted-foreground/20" />
|
||||||
|
<div className="size-2.5 rounded-full bg-muted-foreground/20" />
|
||||||
|
<div className="ml-2 flex-1 max-w-52 h-4 rounded bg-muted-foreground/10" />
|
||||||
|
</div>
|
||||||
|
<Image
|
||||||
|
src={s.light}
|
||||||
|
alt={`Preview ${s.label}`}
|
||||||
|
width={1920}
|
||||||
|
height={1080}
|
||||||
|
className="w-full h-auto dark:hidden"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src={s.dark}
|
||||||
|
alt={`Preview ${s.label}`}
|
||||||
|
width={1920}
|
||||||
|
height={1080}
|
||||||
|
className="w-full h-auto hidden dark:block"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
292
src/features/landing/constants.ts
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
import {
|
||||||
|
RiBankCard2Line,
|
||||||
|
RiBarChartBoxLine,
|
||||||
|
RiCalendarLine,
|
||||||
|
RiCheckLine,
|
||||||
|
RiCodeSSlashLine,
|
||||||
|
RiDatabase2Line,
|
||||||
|
RiDeviceLine,
|
||||||
|
RiDownloadCloudLine,
|
||||||
|
RiEyeOffLine,
|
||||||
|
RiFileTextLine,
|
||||||
|
RiFlashlightLine,
|
||||||
|
RiGitBranchLine,
|
||||||
|
RiLayoutGridLine,
|
||||||
|
RiLineChartLine,
|
||||||
|
RiLockLine,
|
||||||
|
RiNotification3Line,
|
||||||
|
RiPercentLine,
|
||||||
|
RiPieChartLine,
|
||||||
|
RiRobot2Line,
|
||||||
|
RiShieldCheckLine,
|
||||||
|
RiSmartphoneLine,
|
||||||
|
RiStarLine,
|
||||||
|
RiTeamLine,
|
||||||
|
RiTimeLine,
|
||||||
|
RiWalletLine,
|
||||||
|
} from "@remixicon/react";
|
||||||
|
import type { ComponentType } from "react";
|
||||||
|
|
||||||
|
export type FeatureItem = {
|
||||||
|
icon: ComponentType<{ className?: string; style?: React.CSSProperties }>;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
colorVar: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const navbarActionClassName =
|
||||||
|
"border-black/10 bg-transparent text-black/75 shadow-none hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-black/20 data-[state=open]:bg-black/10 data-[state=open]:text-black";
|
||||||
|
|
||||||
|
export const navLinks = [
|
||||||
|
{ href: "#telas", label: "conheça as telas" },
|
||||||
|
{ href: "#funcionalidades", label: "funcionalidades" },
|
||||||
|
{ href: "#mobile", label: "mobile" },
|
||||||
|
{ href: "#stack", label: "stack" },
|
||||||
|
{ href: "#como-usar", label: "como usar" },
|
||||||
|
{ href: "#para-quem-e", label: "para quem é" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const mainFeatures: FeatureItem[] = [
|
||||||
|
{
|
||||||
|
icon: RiWalletLine,
|
||||||
|
title: "Contas e transações",
|
||||||
|
description:
|
||||||
|
"Registre suas contas bancárias, cartões e dinheiro. Adicione receitas, despesas e transferências. Organize por categorias. Extratos detalhados por conta.",
|
||||||
|
colorVar: "var(--data-9)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiPercentLine,
|
||||||
|
title: "Parcelamentos avançados",
|
||||||
|
description:
|
||||||
|
"Controle completo de compras parceladas. Antecipe parcelas com cálculo automático de desconto. Veja análise consolidada de todas as parcelas em aberto.",
|
||||||
|
colorVar: "var(--data-4)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiRobot2Line,
|
||||||
|
title: "Insights com IA",
|
||||||
|
description:
|
||||||
|
"Análises financeiras geradas por IA (Claude, GPT, Gemini). Insights personalizados sobre seus padrões de gastos e recomendações inteligentes.",
|
||||||
|
colorVar: "var(--data-8)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiBarChartBoxLine,
|
||||||
|
title: "Relatórios e gráficos",
|
||||||
|
description:
|
||||||
|
"Dashboard com 20+ widgets interativos. Relatórios detalhados por categoria. Gráficos de evolução e comparativos. Exportação em PDF e Excel.",
|
||||||
|
colorVar: "var(--data-5)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiBankCard2Line,
|
||||||
|
title: "Faturas de cartão",
|
||||||
|
description:
|
||||||
|
"Cadastre seus cartões e acompanhe as faturas por período. Veja o que ainda não foi fechado. Controle limites, vencimentos e fechamentos.",
|
||||||
|
colorVar: "var(--data-1)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiTeamLine,
|
||||||
|
title: "Gestão colaborativa",
|
||||||
|
description:
|
||||||
|
"Compartilhe pagadores com permissões granulares (admin/viewer). Notificações automáticas por e-mail. Colabore em lançamentos compartilhados.",
|
||||||
|
colorVar: "var(--data-3)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const extraFeatures: FeatureItem[] = [
|
||||||
|
{
|
||||||
|
icon: RiPieChartLine,
|
||||||
|
title: "Categorias e orçamentos",
|
||||||
|
description:
|
||||||
|
"Crie categorias personalizadas e defina orçamentos mensais com indicadores visuais.",
|
||||||
|
colorVar: "var(--data-7)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiFileTextLine,
|
||||||
|
title: "Anotações e tarefas",
|
||||||
|
description:
|
||||||
|
"Notas de texto e listas de tarefas com checkboxes. Arquivamento para manter histórico.",
|
||||||
|
colorVar: "var(--data-6)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiCalendarLine,
|
||||||
|
title: "Calendário financeiro",
|
||||||
|
description:
|
||||||
|
"Visualize transações em calendário mensal. Nunca perca prazos de pagamentos.",
|
||||||
|
colorVar: "var(--data-2)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiDownloadCloudLine,
|
||||||
|
title: "Importação em massa",
|
||||||
|
description: "Lance múltiplos lançamentos de uma vez",
|
||||||
|
colorVar: "var(--data-9)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiEyeOffLine,
|
||||||
|
title: "Modo privacidade",
|
||||||
|
description:
|
||||||
|
"Oculte valores sensíveis com um clique. Tema dark/light. Calculadora integrada.",
|
||||||
|
colorVar: "var(--data-4)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiFlashlightLine,
|
||||||
|
title: "Performance otimizada",
|
||||||
|
description: "Sistema rápido e com alta performance",
|
||||||
|
colorVar: "var(--data-5)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const companionBanks = [
|
||||||
|
{ name: "Nubank", logo: "/logos/nubank.png" },
|
||||||
|
{ name: "Itaú", logo: "/logos/itau.png" },
|
||||||
|
{ name: "Inter", logo: "/logos/intermedium.png" },
|
||||||
|
{ name: "Mercado Pago", logo: "/logos/mercadopagocartao.png" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const pwaHighlights: FeatureItem[] = [
|
||||||
|
{
|
||||||
|
icon: RiSmartphoneLine,
|
||||||
|
title: "Instale direto da web",
|
||||||
|
description: "Adicione à tela inicial e abra como app, sem loja.",
|
||||||
|
colorVar: "var(--data-3)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiLayoutGridLine,
|
||||||
|
title: "Acesso rápido ao que importa",
|
||||||
|
description: "Dashboard, inbox e lançamentos a um toque.",
|
||||||
|
colorVar: "var(--data-9)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiFlashlightLine,
|
||||||
|
title: "Experiência mobile mais direta",
|
||||||
|
description: "Modo standalone com navegação limpa e fluida.",
|
||||||
|
colorVar: "var(--data-4)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const pwaCompatList = [
|
||||||
|
{
|
||||||
|
label: "Android",
|
||||||
|
description:
|
||||||
|
"Chrome e Edge — instale pelo banner ou pelo menu do navegador",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "iOS / iPadOS",
|
||||||
|
description: "Safari — adicione à tela inicial pelo menu compartilhar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Desktop",
|
||||||
|
description: "Chrome, Edge e outros — instale pela barra de endereço",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const companionSteps: FeatureItem[] = [
|
||||||
|
{
|
||||||
|
icon: RiNotification3Line,
|
||||||
|
title: "Notificação bancária chega",
|
||||||
|
description: "O Companion intercepta automaticamente",
|
||||||
|
colorVar: "var(--data-1)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiSmartphoneLine,
|
||||||
|
title: "Dados extraídos e enviados",
|
||||||
|
description: "Valor, descrição e banco são identificados",
|
||||||
|
colorVar: "var(--data-4)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiCheckLine,
|
||||||
|
title: "Revise e confirme no OpenMonetis",
|
||||||
|
description: "Pré-lançamentos ficam na inbox para sua aprovação",
|
||||||
|
colorVar: "var(--data-9)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const stackItems = [
|
||||||
|
{
|
||||||
|
icon: RiCodeSSlashLine,
|
||||||
|
title: "Frontend",
|
||||||
|
subtitle: "Next.js, TypeScript, Tailwind CSS, shadcn/ui",
|
||||||
|
description: "Interface moderna e responsiva com React 19 e App Router",
|
||||||
|
colorVar: "var(--data-3)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiDatabase2Line,
|
||||||
|
title: "Backend",
|
||||||
|
subtitle: "PostgreSQL, Drizzle ORM, Better Auth",
|
||||||
|
description: "Banco relacional robusto com type-safe ORM",
|
||||||
|
colorVar: "var(--data-9)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiShieldCheckLine,
|
||||||
|
title: "Segurança",
|
||||||
|
subtitle: "Better Auth com OAuth (Google) e autenticação por email",
|
||||||
|
description: "Sessões seguras e proteção de rotas por middleware",
|
||||||
|
colorVar: "var(--data-1)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiDeviceLine,
|
||||||
|
title: "Deploy",
|
||||||
|
subtitle:
|
||||||
|
"Docker com multi-stage build, health checks e volumes persistentes",
|
||||||
|
description: "Fácil de rodar localmente ou em qualquer servidor",
|
||||||
|
colorVar: "var(--data-5)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const whoIsItForItems: FeatureItem[] = [
|
||||||
|
{
|
||||||
|
icon: RiTimeLine,
|
||||||
|
title: "Tem disciplina de registrar gastos",
|
||||||
|
description:
|
||||||
|
"Não se importa em dedicar alguns minutos por dia ou semana para manter tudo atualizado",
|
||||||
|
colorVar: "var(--data-4)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiLockLine,
|
||||||
|
title: "Quer controle total sobre seus dados",
|
||||||
|
description:
|
||||||
|
"Prefere hospedar seus próprios dados ao invés de depender de serviços terceiros",
|
||||||
|
colorVar: "var(--data-9)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiLineChartLine,
|
||||||
|
title: "Gosta de entender exatamente onde o dinheiro vai",
|
||||||
|
description:
|
||||||
|
"Quer visualizar padrões de gastos e tomar decisões informadas",
|
||||||
|
colorVar: "var(--data-3)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiTimeLine,
|
||||||
|
title: "Não é plug and play",
|
||||||
|
description:
|
||||||
|
"Você vai precisar configurar as coisas, conectar suas contas e ajustar o sistema para o seu jeito de usar.",
|
||||||
|
colorVar: "var(--data-4)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getMetricsItems(stars: number, forks: number) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
icon: RiLayoutGridLine,
|
||||||
|
value: "20+",
|
||||||
|
label: "Widgets no dashboard",
|
||||||
|
colorVar: "var(--data-9)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiShieldCheckLine,
|
||||||
|
value: "100%",
|
||||||
|
label: "Self-hosted",
|
||||||
|
colorVar: "var(--data-1)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiStarLine,
|
||||||
|
value: `${stars}`,
|
||||||
|
label: "Stars no GitHub",
|
||||||
|
colorVar: "var(--data-4)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RiGitBranchLine,
|
||||||
|
value: `${forks}`,
|
||||||
|
label: "Forks no GitHub",
|
||||||
|
colorVar: "var(--data-3)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
59
src/features/landing/images.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Centraliza todos os assets de imagem da landing page.
|
||||||
|
* Para adicionar ou renomear uma imagem, altere apenas aqui.
|
||||||
|
*
|
||||||
|
* Convenção:
|
||||||
|
* - { light, dark } → imagem com variante de tema
|
||||||
|
* - string → imagem única (sem variante dark)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const landingImages = {
|
||||||
|
/** Preview do dashboard no hero da página */
|
||||||
|
hero: {
|
||||||
|
light: "/images/dashboard-preview-light.webp",
|
||||||
|
dark: "/images/dashboard-preview-dark.webp",
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Mockup do app instalado como PWA */
|
||||||
|
pwa: {
|
||||||
|
light: "/images/pwa-preview-light.webp",
|
||||||
|
dark: "/images/pwa-preview-dark.webp",
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Mockup do Companion Android */
|
||||||
|
companion: {
|
||||||
|
light: "/images/companion-preview-light.webp",
|
||||||
|
dark: "/images/companion-preview-dark.webp",
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Screenshots usados nas abas da seção "Conheça as telas" */
|
||||||
|
screenshots: {
|
||||||
|
lancamentos: {
|
||||||
|
light: "/images/preview-lancamentos-light.webp",
|
||||||
|
dark: "/images/preview-lancamentos-dark.webp",
|
||||||
|
},
|
||||||
|
/** Ainda sem print próprio — usando lançamentos como placeholder */
|
||||||
|
preLancamentos: {
|
||||||
|
light: "/images/preview-pre-lancamentos-light.webp",
|
||||||
|
dark: "/images/preview-pre-lancamentos-dark.webp",
|
||||||
|
},
|
||||||
|
/** Ainda sem print próprio — usando lançamentos como placeholder */
|
||||||
|
orcamentos: {
|
||||||
|
light: "/images/preview-orcamentos-light.webp",
|
||||||
|
dark: "/images/preview-orcamentos-dark.webp",
|
||||||
|
},
|
||||||
|
/** Ainda sem print próprio — usando lançamentos como placeholder */
|
||||||
|
parcelas: {
|
||||||
|
light: "/images/preview-parcelas-light.webp",
|
||||||
|
dark: "/images/preview-parcelas-dark.webp",
|
||||||
|
},
|
||||||
|
calendario: {
|
||||||
|
light: "/images/preview-calendario-light.webp",
|
||||||
|
dark: "/images/preview-calendario-dark.webp",
|
||||||
|
},
|
||||||
|
cartoes: {
|
||||||
|
light: "/images/preview-cartao-light.webp",
|
||||||
|
dark: "/images/preview-cartao-dark.webp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
19
src/features/landing/queries.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export async function fetchGitHubStats(): Promise<{
|
||||||
|
stars: number;
|
||||||
|
forks: number;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
"https://api.github.com/repos/felipegcoutinho/openmonetis",
|
||||||
|
{ next: { revalidate: 3600 } },
|
||||||
|
);
|
||||||
|
if (!res.ok) return { stars: 200, forks: 60 };
|
||||||
|
const data = await res.json();
|
||||||
|
return {
|
||||||
|
stars: data.stargazers_count as number,
|
||||||
|
forks: data.forks_count as number,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { stars: 200, forks: 60 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,9 @@ import { DEFAULT_PAYER_AVATAR } from "@/shared/lib/payers/constants";
|
|||||||
|
|
||||||
const AVATAR_DIRECTORY = path.join(process.cwd(), "public", "avatars");
|
const AVATAR_DIRECTORY = path.join(process.cwd(), "public", "avatars");
|
||||||
const AVATAR_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".svg", ".webp"]);
|
const AVATAR_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".svg", ".webp"]);
|
||||||
|
let avatarOptionsPromise: Promise<string[]> | null = null;
|
||||||
|
|
||||||
/**
|
async function readAvatarOptions() {
|
||||||
* Loads available avatar files from the public/avatars directory
|
|
||||||
* @returns Array of unique avatar filenames sorted alphabetically
|
|
||||||
*/
|
|
||||||
export async function loadAvatarOptions() {
|
|
||||||
try {
|
try {
|
||||||
const files = await readdir(AVATAR_DIRECTORY, { withFileTypes: true });
|
const files = await readdir(AVATAR_DIRECTORY, { withFileTypes: true });
|
||||||
|
|
||||||
@@ -28,3 +25,12 @@ export async function loadAvatarOptions() {
|
|||||||
return [DEFAULT_PAYER_AVATAR];
|
return [DEFAULT_PAYER_AVATAR];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads available avatar files from the public/avatars directory
|
||||||
|
* @returns Array of unique avatar filenames sorted alphabetically
|
||||||
|
*/
|
||||||
|
export async function loadAvatarOptions() {
|
||||||
|
avatarOptionsPromise ??= readAvatarOptions();
|
||||||
|
return avatarOptionsPromise;
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ export function Logo({
|
|||||||
alt="OpenMonetis"
|
alt="OpenMonetis"
|
||||||
width={32}
|
width={32}
|
||||||
height={32}
|
height={32}
|
||||||
className={cn("object-contain", !colorIcon && "brightness-0 saturate-0")}
|
className={cn(
|
||||||
|
"object-contain",
|
||||||
|
!colorIcon && "brightness-0 saturate-0",
|
||||||
|
)}
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -3,12 +3,9 @@ import path from "node:path";
|
|||||||
|
|
||||||
const LOGOS_DIRECTORY = path.join(process.cwd(), "public", "logos");
|
const LOGOS_DIRECTORY = path.join(process.cwd(), "public", "logos");
|
||||||
const LOGO_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".svg", ".webp"]);
|
const LOGO_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".svg", ".webp"]);
|
||||||
|
let logoOptionsPromise: Promise<string[]> | null = null;
|
||||||
|
|
||||||
/**
|
async function readLogoOptions() {
|
||||||
* Loads available logo files from the public/logos directory
|
|
||||||
* @returns Array of logo filenames sorted alphabetically
|
|
||||||
*/
|
|
||||||
export async function loadLogoOptions() {
|
|
||||||
try {
|
try {
|
||||||
const files = await readdir(LOGOS_DIRECTORY, { withFileTypes: true });
|
const files = await readdir(LOGOS_DIRECTORY, { withFileTypes: true });
|
||||||
|
|
||||||
@@ -21,3 +18,8 @@ export async function loadLogoOptions() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadLogoOptions() {
|
||||||
|
logoOptionsPromise ??= readLogoOptions();
|
||||||
|
return logoOptionsPromise;
|
||||||
|
}
|
||||||
|
|||||||