Merge branch 'main' into feat/fix-ui

This commit is contained in:
Alexsandro
2026-04-16 11:52:37 -03:00
committed by GitHub
187 changed files with 4380 additions and 2719 deletions

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiBankLine />}
title="Contas"

View File

@@ -0,0 +1,26 @@
import { RiAttachmentLine } from "@remixicon/react";
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
import PageDescription from "@/shared/components/page-description";
export const metadata = {
title: "Anexos",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section className="space-y-6">
<PageDescription
icon={<RiAttachmentLine />}
title="Anexos"
subtitle="Gerencie os anexos das suas transações"
/>
<MonthNavigation />
{children}
</section>
);
}

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiBarChart2Line />}
title="Orçamentos"

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiCalendarEventLine />}
title="Calendário"

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiBankCard2Line />}
title="Cartões"

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiPriceTag3Line />}
title="Categorias"

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiHistoryLine />}
title="Changelog"

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiAtLine />}
title="Pré-Lançamentos"

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiSparklingLine />}
title="Insights"

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiTodoLine />}
title="Anotações"

View File

@@ -80,6 +80,8 @@ const EMPTY_FILTERS: TransactionSearchFilters = {
categoryFilter: null,
accountCardFilter: null,
searchFilter: null,
settledFilter: null,
attachmentFilter: null,
};
const createEmptySlugMaps = (): SlugMaps => ({

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiGroupLine />}
title="Pagadores"

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiBankCard2Line />}
title="Uso de Cartões"

View File

@@ -71,7 +71,7 @@ export default async function RelatorioCartoesPage({
<div className="flex size-14 items-center justify-center rounded-full bg-muted mb-4">
<RiBankCard2Line className="size-7 text-muted-foreground" />
</div>
<p className="text-base font-medium">Nenhum cartão selecionado</p>
<p className="text-base font-semibold">Nenhum cartão selecionado</p>
<p className="text-sm text-muted-foreground mt-1">
Selecione um cartão para ver os detalhes de uso.
</p>

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiFileChartLine />}
title="Tendências"

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiStore2Line />}
title="Top Estabelecimentos"

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiSecurePaymentLine />}
title="Análise de Parcelas"

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiSettings2Line />}
title="Ajustes"

View File

@@ -68,7 +68,7 @@ export default async function Page() {
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-xl font-medium mb-1">Preferências</h2>
<h2 className="text-xl font-semibold mb-1">Preferências</h2>
<p className="text-sm text-muted-foreground">
Personalize sua experiência no OpenMonetis ajustando as
configurações de acordo com suas necessidades.
@@ -93,7 +93,9 @@ export default async function Page() {
<div className="space-y-4">
<div>
<div className="flex items-center gap-2 mb-1">
<h2 className="text-xl font-medium">OpenMonetis Companion</h2>
<h2 className="text-xl font-semibold">
OpenMonetis Companion
</h2>
<span className="inline-flex items-center gap-1 rounded-full bg-success/10 px-2 py-0.5 text-xs font-medium text-success dark:bg-success/10">
<RiAndroidLine className="h-3 w-3" />
Android
@@ -115,7 +117,7 @@ export default async function Page() {
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-xl font-medium mb-1">Alterar nome</h2>
<h2 className="text-xl font-semibold mb-1">Alterar nome</h2>
<p className="text-sm text-muted-foreground">
Atualize como seu nome aparece no OpenMonetis. Esse nome pode
ser exibido em diferentes seções do app e em comunicações.
@@ -131,7 +133,7 @@ export default async function Page() {
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-xl font-medium mb-1">Alterar senha</h2>
<h2 className="text-xl font-semibold mb-1">Alterar senha</h2>
<p className="text-sm text-muted-foreground">
Defina uma nova senha para sua conta. Guarde-a em local
seguro.
@@ -147,7 +149,7 @@ export default async function Page() {
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-xl font-medium mb-1">Passkeys</h2>
<h2 className="text-xl font-semibold mb-1">Passkeys</h2>
<p className="text-sm text-muted-foreground">
Passkeys permitem login sem senha, usando biometria (Face ID,
Touch ID, Windows Hello) ou chaves de segurança.
@@ -163,7 +165,7 @@ export default async function Page() {
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-xl font-medium mb-1">Alterar e-mail</h2>
<h2 className="text-xl font-semibold mb-1">Alterar e-mail</h2>
<p className="text-sm text-muted-foreground">
Atualize o e-mail associado à sua conta. Você precisará
confirmar os links enviados para o novo e também para o e-mail
@@ -183,9 +185,7 @@ export default async function Page() {
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-xl font-medium mb-1 text-destructive">
Ações perigosas
</h2>
<h2 className="text-xl font-semibold mb-1">Ações perigosas</h2>
<p className="text-sm text-muted-foreground">
Você pode zerar os dados do OpenMonetis e manter seu acesso,
ou excluir sua conta inteira de forma irreversível.

View File

@@ -11,7 +11,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<section className="space-y-6 pt-4">
<section className="space-y-6">
<PageDescription
icon={<RiArrowLeftRightLine />}
title="Lançamentos"

View File

@@ -120,13 +120,13 @@ export default async function Page() {
</div>
<div className="max-w-8xl mx-auto px-4 relative">
<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-4xl flex-col items-center text-center gap-5 md:gap-6 pb-10 md:pb-14">
<Badge variant="outline">
<RiGithubFill className="size-4 mr-1" />
Projeto Open Source
</Badge>
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-medium tracking-tight">
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-semibold">
Suas finanças,
<span className="text-primary"> do seu jeito</span>
</h1>
@@ -207,7 +207,7 @@ export default async function Page() {
className="flex flex-col items-center text-center gap-1.5"
>
<Icon className="size-5" style={{ color: colorVar }} />
<span className="text-2xl md:text-3xl font-medium">
<span className="text-2xl md:text-3xl font-semibold">
{value}
</span>
<span className="text-xs md:text-sm text-muted-foreground">
@@ -229,7 +229,7 @@ export default async function Page() {
<Badge variant="outline" className="mb-4">
Conheça as telas
</Badge>
<h2 className="text-2xl sm:text-3xl md:text-4xl font-medium tracking-tight mb-3 md:mb-4">
<h2 className="text-2xl sm:text-3xl md:text-4xl mb-3 md:mb-4 font-semibold">
Veja o que você pode fazer
</h2>
<p className="text-base md:text-lg text-muted-foreground max-w-2xl mx-auto px-4 sm:px-0">
@@ -254,7 +254,7 @@ export default async function Page() {
<Badge variant="outline" className="mb-4">
O que tem aqui
</Badge>
<h2 className="text-2xl sm:text-3xl md:text-4xl font-medium tracking-tight mb-3 md:mb-4">
<h2 className="text-2xl sm:text-3xl md:text-4xl mb-3 md:mb-4 font-semibold">
Funcionalidades que importam
</h2>
<p className="text-base md:text-lg text-muted-foreground max-w-2xl mx-auto px-4 sm:px-0">
@@ -282,7 +282,7 @@ export default async function Page() {
/>
</div>
<div>
<h3 className="font-medium text-base md:text-lg mb-1.5 md:mb-2">
<h3 className="font-semibold text-base md:text-lg mb-1.5 md:mb-2">
{feature.title}
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
@@ -298,7 +298,7 @@ export default async function Page() {
<AnimateOnScroll>
<div className="mt-8 md:mt-12">
<h3 className="text-sm font-medium text-center mb-4 md:mb-6 text-muted-foreground">
<h3 className="text-sm font-semibold text-center mb-4 md:mb-6 text-muted-foreground">
Também inclui
</h3>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
@@ -319,7 +319,7 @@ export default async function Page() {
/>
</div>
<div className="min-w-0">
<h4 className="font-medium text-sm mb-0.5">
<h4 className="font-semibold text-sm mb-0.5">
{feature.title}
</h4>
<p className="text-xs text-muted-foreground leading-relaxed">
@@ -346,7 +346,7 @@ export default async function Page() {
<RiSmartphoneLine className="size-3.5 mr-1" />
Mobile
</Badge>
<h2 className="text-2xl sm:text-3xl md:text-4xl font-medium tracking-tight mb-3 md:mb-4">
<h2 className="text-2xl sm:text-3xl md:text-4xl mb-3 md:mb-4 font-semibold">
Use o OpenMonetis no celular sem perder o fluxo
</h2>
<p className="text-base md:text-lg text-muted-foreground max-w-2xl mx-auto px-4 sm:px-0">
@@ -529,7 +529,7 @@ export default async function Page() {
<Badge variant="outline" className="mb-4">
Stack técnica
</Badge>
<h2 className="text-2xl sm:text-3xl md:text-4xl font-medium tracking-tight mb-3 md:mb-4">
<h2 className="text-2xl sm:text-3xl md:text-4xl mb-3 md:mb-4 font-semibold">
O que roda por baixo
</h2>
<p className="text-base md:text-lg text-muted-foreground max-w-2xl mx-auto px-4 sm:px-0">
@@ -556,7 +556,7 @@ export default async function Page() {
/>
</div>
<div>
<h3 className="font-medium text-base md:text-lg mb-1.5 md:mb-2">
<h3 className="font-semibold text-base md:text-lg mb-1.5 md:mb-2">
{item.title}
</h3>
<p className="text-sm text-muted-foreground mb-2 md:mb-3">
@@ -582,7 +582,7 @@ export default async function Page() {
<Badge variant="outline" className="mb-4">
Como usar
</Badge>
<h2 className="text-2xl sm:text-3xl md:text-4xl font-medium tracking-tight mb-3 md:mb-4">
<h2 className="text-2xl sm:text-3xl md:text-4xl mb-3 md:mb-4 font-semibold">
Rode no seu computador
</h2>
<p className="text-base md:text-lg text-muted-foreground px-4 sm:px-0">
@@ -617,7 +617,7 @@ export default async function Page() {
<Badge variant="outline" className="mb-4">
Para quem é?
</Badge>
<h2 className="text-2xl sm:text-3xl md:text-4xl font-medium tracking-tight mb-3 md:mb-4">
<h2 className="text-2xl sm:text-3xl md:text-4xl mb-3 md:mb-4 font-semibold">
Feito para quem gosta de controle
</h2>
<p className="text-base md:text-lg text-muted-foreground px-4 sm:px-0">
@@ -644,7 +644,7 @@ export default async function Page() {
/>
</div>
<div>
<h3 className="font-medium mb-1">{item.title}</h3>
<h3 className="font-semibold mb-1">{item.title}</h3>
<p className="text-xs sm:text-sm text-muted-foreground">
{item.description}
</p>
@@ -664,7 +664,7 @@ export default async function Page() {
<div className="max-w-8xl mx-auto px-4">
<AnimateOnScroll>
<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-medium tracking-tight mb-3 md:mb-4">
<h2 className="text-2xl sm:text-3xl md:text-4xl mb-3 md:mb-4 font-semibold">
Pronto para testar?
</h2>
<p className="text-base md:text-lg text-muted-foreground mb-6 md:mb-8">
@@ -715,7 +715,7 @@ export default async function Page() {
</div>
<div>
<h3 className="font-medium mb-3 md:mb-4">Projeto</h3>
<h3 className="font-semibold mb-3 md:mb-4">Projeto</h3>
<ul className="space-y-2.5 md:space-y-3 text-sm text-muted-foreground">
<li>
<Link
@@ -749,7 +749,7 @@ export default async function Page() {
</div>
<div>
<h3 className="font-medium mb-3 md:mb-4">Companion</h3>
<h3 className="font-semibold mb-3 md:mb-4">Companion</h3>
<ul className="space-y-2.5 md:space-y-3 text-sm text-muted-foreground">
<li>
<Link

View File

@@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
import { getOptionalUserSession } from "@/shared/lib/auth/server";
import { fetchEstablishmentLogoDomain } from "@/shared/lib/logo/establishment-logo-queries";
/**
* GET /api/logo/mapping?name={name}
*
* Retorna o domínio Logo.dev salvo pelo usuário para um estabelecimento.
* Usado pelo EstablishmentLogo para hidratar o domain salvo no banco.
*/
export async function GET(request: Request) {
const session = await getOptionalUserSession();
if (!session) {
return NextResponse.json({ domain: null }, { status: 200 });
}
const { searchParams } = new URL(request.url);
const name = searchParams.get("name")?.trim();
if (!name) {
return NextResponse.json({ domain: null }, { status: 200 });
}
const domain = await fetchEstablishmentLogoDomain(session.user.id, name);
return NextResponse.json({ domain });
}

View File

@@ -0,0 +1,80 @@
import { NextResponse } from "next/server";
import { getOptionalUserSession } from "@/shared/lib/auth/server";
const LOGO_DEV_SEARCH_URL = "https://api.logo.dev/search";
interface LogoResult {
name: string;
domain: string;
}
async function searchByStrategy(
q: string,
strategy: "match" | "typeahead",
secretKey: string,
): Promise<LogoResult[]> {
try {
const url = `${LOGO_DEV_SEARCH_URL}?q=${encodeURIComponent(q)}&strategy=${strategy}`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${secretKey}` },
next: { revalidate: 3600 },
});
if (!res.ok) return [];
const data = await res.json();
return Array.isArray(data) ? data : [];
} catch {
return [];
}
}
/**
* GET /api/logo/search?q={name}
*
* Proxy seguro para a Logo.dev Brand Search API.
* Faz duas buscas paralelas (match + typeahead) e retorna até 20 resultados únicos.
* Usa LOGO_DEV_SECRET_KEY server-side — nunca exposta ao cliente.
*/
export async function GET(request: Request) {
const session = await getOptionalUserSession();
if (!session) {
return NextResponse.json({ error: "Não autorizado." }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const q = searchParams.get("q")?.trim();
if (!q) {
return NextResponse.json(
{ error: "Parâmetro q obrigatório." },
{ status: 400 },
);
}
const secretKey = process.env.LOGO_DEV_SECRET_KEY;
if (!secretKey) {
return NextResponse.json(
{ error: "Logo.dev não configurado." },
{ status: 503 },
);
}
// Duas buscas paralelas para maximizar resultados (cada uma retorna até 10)
const [matchResults, typeaheadResults] = await Promise.all([
searchByStrategy(q, "match", secretKey),
searchByStrategy(q, "typeahead", secretKey),
]);
// Mescla e deduplica por domain, mantendo ordem (match tem prioridade)
const seen = new Set<string>();
const merged: LogoResult[] = [];
for (const result of [...matchResults, ...typeaheadResults]) {
if (!seen.has(result.domain)) {
seen.add(result.domain);
merged.push(result);
if (merged.length >= 20) break;
}
}
return NextResponse.json(merged);
}

View File

@@ -177,7 +177,7 @@
}
@theme inline {
--default-font-family: var(--font-america);
--default-font-family: var(--font-inter);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);

View File

@@ -4,7 +4,7 @@ import { QueryProvider } from "@/shared/components/providers/query-provider";
import { ThemeProvider } from "@/shared/components/providers/theme-provider";
import { Toaster } from "@/shared/components/ui/sonner";
import "./globals.css";
import { america } from "@/public/fonts/font_index";
import { inter } from "@/public/fonts/font_index";
export const metadata: Metadata = {
title: {
@@ -24,19 +24,23 @@ export default function RootLayout({
<html
data-scroll-behavior="smooth"
lang="pt-BR"
className={`${america.variable} ${america.className} `}
className={`${inter.variable}`}
suppressHydrationWarning
>
<head>
<meta name="apple-mobile-web-app-title" content="OpenMonetis" />
<script
defer
src="https://umami.felipecoutinho.com/script.js"
data-website-id="ea438854-a014-42ea-b416-0a8321471f0f"
data-domains="openmonetis.com"
/>
{process.env.UMAMI_URL && process.env.UMAMI_WEBSITE_ID && (
<script
defer
src={`${process.env.UMAMI_URL}/script.js`}
data-website-id={process.env.UMAMI_WEBSITE_ID}
{...(process.env.UMAMI_DOMAINS
? { "data-domains": process.env.UMAMI_DOMAINS }
: {})}
/>
)}
</head>
<body className="antialiased" suppressHydrationWarning>
<body className="subpixel-antialiased" suppressHydrationWarning>
<ThemeProvider attribute="class" defaultTheme="light">
<QueryProvider>
<Suspense>{children}</Suspense>