feat(v1.5.3): status de pagamento no painel do pagador + SEO landing + WebP
- Card de Status de Pagamento com totais pagos/pendentes e lista de boletos individuais - Validação obrigatória de categoria/conta/cartão no dialog de lançamento (client + server) - SEO completo na landing: Open Graph, Twitter Card, JSON-LD, sitemap.xml, robots.txt - Imagens convertidas de PNG para WebP (performance) - HTML lang corrigido para pt-BR; template de título dinâmico Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -182,6 +182,30 @@ const refineLancamento = (
|
||||
data: z.infer<typeof baseFields> & { id?: string },
|
||||
ctx: z.RefinementCtx,
|
||||
) => {
|
||||
if (!data.categoriaId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["categoriaId"],
|
||||
message: "Selecione uma categoria.",
|
||||
});
|
||||
}
|
||||
|
||||
if (data.paymentMethod === "Cartão de crédito") {
|
||||
if (!data.cartaoId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["cartaoId"],
|
||||
message: "Selecione o cartão.",
|
||||
});
|
||||
}
|
||||
} else if (!data.contaId) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["contaId"],
|
||||
message: "Selecione a conta.",
|
||||
});
|
||||
}
|
||||
|
||||
if (data.condition === "Parcelado") {
|
||||
if (!data.installmentCount) {
|
||||
ctx.addIssue({
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
RiBankCard2Line,
|
||||
RiBarcodeLine,
|
||||
RiWallet3Line,
|
||||
} from "@remixicon/react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||
import { LancamentosPage as LancamentosSection } from "@/components/lancamentos/page/lancamentos-page";
|
||||
@@ -13,9 +18,13 @@ import { PagadorHistoryCard } from "@/components/pagadores/details/pagador-histo
|
||||
import { PagadorInfoCard } from "@/components/pagadores/details/pagador-info-card";
|
||||
import { PagadorLeaveShareCard } from "@/components/pagadores/details/pagador-leave-share-card";
|
||||
import { PagadorMonthlySummaryCard } from "@/components/pagadores/details/pagador-monthly-summary-card";
|
||||
import { PagadorBoletoCard } from "@/components/pagadores/details/pagador-payment-method-cards";
|
||||
import {
|
||||
PagadorBoletoCard,
|
||||
PagadorPaymentStatusCard,
|
||||
} from "@/components/pagadores/details/pagador-payment-method-cards";
|
||||
import { PagadorSharingCard } from "@/components/pagadores/details/pagador-sharing-card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import WidgetCard from "@/components/widget-card";
|
||||
import type { pagadores } from "@/db/schema";
|
||||
import { getUserId } from "@/lib/auth/server";
|
||||
import {
|
||||
@@ -34,10 +43,12 @@ import {
|
||||
} from "@/lib/lancamentos/page-helpers";
|
||||
import { getPagadorAccess } from "@/lib/pagadores/access";
|
||||
import {
|
||||
fetchPagadorBoletoItems,
|
||||
fetchPagadorBoletoStats,
|
||||
fetchPagadorCardUsage,
|
||||
fetchPagadorHistory,
|
||||
fetchPagadorMonthlyBreakdown,
|
||||
fetchPagadorPaymentStatus,
|
||||
} from "@/lib/pagadores/details";
|
||||
import { parsePeriodParam } from "@/lib/utils/period";
|
||||
import {
|
||||
@@ -152,6 +163,8 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
historyData,
|
||||
cardUsage,
|
||||
boletoStats,
|
||||
boletoItems,
|
||||
paymentStatus,
|
||||
shareRows,
|
||||
currentUserShare,
|
||||
estabelecimentos,
|
||||
@@ -177,6 +190,16 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
pagadorId: pagador.id,
|
||||
period: selectedPeriod,
|
||||
}),
|
||||
fetchPagadorBoletoItems({
|
||||
userId: dataOwnerId,
|
||||
pagadorId: pagador.id,
|
||||
period: selectedPeriod,
|
||||
}),
|
||||
fetchPagadorPaymentStatus({
|
||||
userId: dataOwnerId,
|
||||
pagadorId: pagador.id,
|
||||
period: selectedPeriod,
|
||||
}),
|
||||
sharesPromise,
|
||||
currentUserSharePromise,
|
||||
getRecentEstablishmentsAction(),
|
||||
@@ -308,7 +331,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="painel" className="space-y-4">
|
||||
<section className="grid gap-4 lg:grid-cols-2">
|
||||
<section className="grid gap-3 lg:grid-cols-2">
|
||||
<PagadorMonthlySummaryCard
|
||||
periodLabel={periodLabel}
|
||||
breakdown={monthlyBreakdown}
|
||||
@@ -316,9 +339,28 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
<PagadorHistoryCard data={historyData} />
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 lg:grid-cols-2">
|
||||
<PagadorCardUsageCard items={cardUsage} />
|
||||
<PagadorBoletoCard stats={boletoStats} />
|
||||
<section className="grid gap-3 lg:grid-cols-3">
|
||||
<WidgetCard
|
||||
title="Minhas Faturas"
|
||||
subtitle="Valores por cartão neste período"
|
||||
icon={<RiBankCard2Line className="size-4" />}
|
||||
>
|
||||
<PagadorCardUsageCard items={cardUsage} />
|
||||
</WidgetCard>
|
||||
<WidgetCard
|
||||
title="Boletos"
|
||||
subtitle="Boletos registrados neste período"
|
||||
icon={<RiBarcodeLine className="size-4" />}
|
||||
>
|
||||
<PagadorBoletoCard items={boletoItems} />
|
||||
</WidgetCard>
|
||||
<WidgetCard
|
||||
title="Status de Pagamento"
|
||||
subtitle="Situação das despesas no período"
|
||||
icon={<RiWallet3Line className="size-4" />}
|
||||
>
|
||||
<PagadorPaymentStatusCard data={paymentStatus} />
|
||||
</WidgetCard>
|
||||
</section>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
95
app/(landing-page)/layout.tsx
Normal file
95
app/(landing-page)/layout.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
const BASE_URL = process.env.PUBLIC_DOMAIN
|
||||
? `https://${process.env.PUBLIC_DOMAIN}`
|
||||
: "https://openmonetis.com";
|
||||
|
||||
const TITLE = "OpenMonetis | Finanças pessoais self-hosted e open source";
|
||||
const DESCRIPTION =
|
||||
"Aplicativo self-hosted de finanças pessoais. Controle lançamentos, cartões, orçamentos e categorias com total privacidade. Open source e gratuito.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(BASE_URL),
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
keywords: [
|
||||
"finanças pessoais",
|
||||
"controle financeiro",
|
||||
"self-hosted",
|
||||
"open source",
|
||||
"gestão financeira",
|
||||
"orçamento pessoal",
|
||||
"lançamentos financeiros",
|
||||
"cartão de crédito",
|
||||
"planejamento financeiro",
|
||||
],
|
||||
alternates: {
|
||||
canonical: "/",
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "pt_BR",
|
||||
url: "/",
|
||||
siteName: "OpenMonetis",
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [
|
||||
{
|
||||
url: "/dashboard-preview-light.webp",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
alt: "OpenMonetis — Dashboard de finanças pessoais",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: ["/dashboard-preview-light.webp"],
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function LandingLayout({ children }: { children: ReactNode }) {
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
name: "OpenMonetis",
|
||||
applicationCategory: "FinanceApplication",
|
||||
operatingSystem: "Web",
|
||||
offers: {
|
||||
"@type": "Offer",
|
||||
price: "0",
|
||||
priceCurrency: "BRL",
|
||||
},
|
||||
description: DESCRIPTION,
|
||||
url: BASE_URL,
|
||||
isAccessibleForFree: true,
|
||||
author: {
|
||||
"@type": "Organization",
|
||||
name: "OpenMonetis",
|
||||
url: BASE_URL,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -120,20 +120,20 @@ const screenshotSections = [
|
||||
{
|
||||
title: "Lançamentos",
|
||||
description: "Registre e organize todas as suas transações financeiras",
|
||||
lightSrc: "/preview-lancamentos-light.png",
|
||||
darkSrc: "/preview-lancamentos-dark.png",
|
||||
lightSrc: "/preview-lancamentos-light.webp",
|
||||
darkSrc: "/preview-lancamentos-dark.webp",
|
||||
},
|
||||
{
|
||||
title: "Calendário",
|
||||
description: "Visualize suas finanças no calendário mensal",
|
||||
lightSrc: "/preview-calendario-light.png",
|
||||
darkSrc: "/preview-calendario-dark.png",
|
||||
lightSrc: "/preview-calendario-light.webp",
|
||||
darkSrc: "/preview-calendario-dark.webp",
|
||||
},
|
||||
{
|
||||
title: "Cartões",
|
||||
description: "Acompanhe faturas, limites e vencimentos dos seus cartões",
|
||||
lightSrc: "/preview-cartao-light.png",
|
||||
darkSrc: "/preview-cartao-dark.png",
|
||||
lightSrc: "/preview-cartao-light.webp",
|
||||
darkSrc: "/preview-cartao-dark.webp",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -355,7 +355,7 @@ export default async function Page() {
|
||||
<AnimateOnScroll>
|
||||
<div>
|
||||
<Image
|
||||
src="/dashboard-preview-light.png"
|
||||
src="/dashboard-preview-light.webp"
|
||||
alt="openmonetis Dashboard Preview"
|
||||
width={1920}
|
||||
height={1080}
|
||||
@@ -363,7 +363,7 @@ export default async function Page() {
|
||||
priority
|
||||
/>
|
||||
<Image
|
||||
src="/dashboard-preview-dark.png"
|
||||
src="/dashboard-preview-dark.webp"
|
||||
alt="openmonetis Dashboard Preview"
|
||||
width={1920}
|
||||
height={1080}
|
||||
@@ -609,7 +609,7 @@ export default async function Page() {
|
||||
<div className="order-1 md:order-2 flex items-center justify-center">
|
||||
<div className="w-full max-w-[220px] md:max-w-[260px]">
|
||||
<Image
|
||||
src="/openmonetis_companion.png"
|
||||
src="/openmonetis_companion.webp"
|
||||
alt="OpenMonetis Companion App"
|
||||
width={1080}
|
||||
height={2217}
|
||||
|
||||
@@ -7,7 +7,10 @@ import { allFontVariables } from "@/public/fonts/font_index";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "OpenMonetis | Suas finanças, do seu jeito",
|
||||
title: {
|
||||
default: "OpenMonetis | Suas finanças, do seu jeito",
|
||||
template: "%s | OpenMonetis",
|
||||
},
|
||||
description:
|
||||
"Controle suas finanças pessoais de forma simples e transparente.",
|
||||
};
|
||||
@@ -18,7 +21,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className={allFontVariables} suppressHydrationWarning>
|
||||
<html lang="pt-BR" className={allFontVariables} suppressHydrationWarning>
|
||||
<head>
|
||||
<meta name="apple-mobile-web-app-title" content="OpenMonetis" />
|
||||
</head>
|
||||
|
||||
36
app/robots.ts
Normal file
36
app/robots.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
const BASE_URL = process.env.PUBLIC_DOMAIN
|
||||
? `https://${process.env.PUBLIC_DOMAIN}`
|
||||
: "https://openmonetis.com";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
disallow: [
|
||||
"/dashboard",
|
||||
"/lancamentos",
|
||||
"/contas",
|
||||
"/cartoes",
|
||||
"/categorias",
|
||||
"/orcamentos",
|
||||
"/pagadores",
|
||||
"/anotacoes",
|
||||
"/insights",
|
||||
"/calendario",
|
||||
"/consultor",
|
||||
"/ajustes",
|
||||
"/relatorios",
|
||||
"/top-estabelecimentos",
|
||||
"/pre-lancamentos",
|
||||
"/login",
|
||||
"/api/",
|
||||
],
|
||||
},
|
||||
],
|
||||
sitemap: `${BASE_URL}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
16
app/sitemap.ts
Normal file
16
app/sitemap.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
const BASE_URL = process.env.PUBLIC_DOMAIN
|
||||
? `https://${process.env.PUBLIC_DOMAIN}`
|
||||
: "https://openmonetis.com";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [
|
||||
{
|
||||
url: BASE_URL,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user