mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 02:51:46 +00:00
perf(logos): pré-resolver mapeamentos Logo.dev no servidor
Cada EstablishmentLogo dispara um GET para /api/logo/mapping por nome único (deduplicado pelo React Query, mas ainda N requests por página). Em /dashboard, /transactions e /payers/[payerId] agora fazemos uma única query SQL em batch (fetchEstablishmentLogoMap) e semeamos o cache do React Query antes do primeiro render via novo LogoPrefetchProvider — eliminando os requests da rede. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,10 +2,13 @@ import { connection } from "next/server";
|
||||
import { DashboardGridEditable } from "@/features/dashboard/components/dashboard-grid-editable";
|
||||
import { DashboardMetricsCards } from "@/features/dashboard/components/dashboard-metrics-cards";
|
||||
import { DashboardWelcome } from "@/features/dashboard/components/dashboard-welcome";
|
||||
import { extractDashboardLogoNames } from "@/features/dashboard/extract-logo-names";
|
||||
import { fetchDashboardPageData } from "@/features/dashboard/page-data-queries";
|
||||
import { getSingleParam } from "@/features/transactions/page-helpers";
|
||||
import { LogoPrefetchProvider } from "@/shared/components/entity-avatar";
|
||||
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
||||
import { getUser } from "@/shared/lib/auth/server";
|
||||
import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server";
|
||||
import { parsePeriodParam } from "@/shared/utils/period";
|
||||
|
||||
type PageSearchParams = Promise<Record<string, string | string[] | undefined>>;
|
||||
@@ -25,17 +28,24 @@ export default async function Page({ searchParams }: PageProps) {
|
||||
await fetchDashboardPageData(user.id, selectedPeriod);
|
||||
const { dashboardWidgets } = preferences;
|
||||
|
||||
const logoMappings = await prefetchLogoMappings(
|
||||
user.id,
|
||||
extractDashboardLogoNames(dashboardData),
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-4">
|
||||
<DashboardWelcome name={user.name} />
|
||||
<MonthNavigation />
|
||||
<DashboardMetricsCards metrics={dashboardData.metrics} />
|
||||
<LogoPrefetchProvider mappings={logoMappings}>
|
||||
<DashboardGridEditable
|
||||
data={dashboardData}
|
||||
period={selectedPeriod}
|
||||
initialPreferences={dashboardWidgets}
|
||||
quickActionOptions={quickActionOptions}
|
||||
/>
|
||||
</LogoPrefetchProvider>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
fetchRecentEstablishments,
|
||||
fetchTransactionFilterSources,
|
||||
} from "@/features/transactions/queries";
|
||||
import { LogoPrefetchProvider } from "@/shared/components/entity-avatar";
|
||||
import { ExpandableWidgetCard } from "@/shared/components/expandable-widget-card";
|
||||
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
||||
import {
|
||||
@@ -50,6 +51,7 @@ import {
|
||||
TabsTrigger,
|
||||
} from "@/shared/components/ui/tabs";
|
||||
import { getUserId } from "@/shared/lib/auth/server";
|
||||
import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server";
|
||||
import { getPayerAccess } from "@/shared/lib/payers/access";
|
||||
import {
|
||||
fetchPagadorBoletoItems,
|
||||
@@ -82,6 +84,7 @@ const EMPTY_FILTERS: TransactionSearchFilters = {
|
||||
searchFilter: null,
|
||||
settledFilter: null,
|
||||
attachmentFilter: null,
|
||||
dividedFilter: null,
|
||||
};
|
||||
|
||||
const createEmptySlugMaps = (): SlugMaps => ({
|
||||
@@ -307,10 +310,16 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
lancamentoCount: transactionData.length,
|
||||
};
|
||||
|
||||
const logoMappings = await prefetchLogoMappings(dataOwnerId, [
|
||||
...transactionData.map((t) => t.name),
|
||||
...boletoItems.map((b) => b.name),
|
||||
]);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
<MonthNavigation />
|
||||
|
||||
<LogoPrefetchProvider mappings={logoMappings}>
|
||||
<Tabs defaultValue="profile" className="w-full">
|
||||
<TabsList className="mb-2">
|
||||
<TabsTrigger value="profile">Perfil</TabsTrigger>
|
||||
@@ -396,7 +405,9 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||
importPayerOptions={loggedUserOptionSets?.payerOptions}
|
||||
importSplitPayerOptions={loggedUserOptionSets?.splitPayerOptions}
|
||||
importSplitPayerOptions={
|
||||
loggedUserOptionSets?.splitPayerOptions
|
||||
}
|
||||
importDefaultPayerId={loggedUserOptionSets?.defaultPayerId}
|
||||
importAccountOptions={loggedUserOptionSets?.accountOptions}
|
||||
importCardOptions={loggedUserOptionSets?.cardOptions}
|
||||
@@ -405,6 +416,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
</section>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</LogoPrefetchProvider>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ import {
|
||||
fetchTransactionFilterSources,
|
||||
fetchTransactionsPage,
|
||||
} from "@/features/transactions/queries";
|
||||
import { LogoPrefetchProvider } from "@/shared/components/entity-avatar";
|
||||
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
|
||||
import { getUserId } from "@/shared/lib/auth/server";
|
||||
import { prefetchLogoMappings } from "@/shared/lib/logo/prefetch-server";
|
||||
import { parsePeriodParam } from "@/shared/utils/period";
|
||||
|
||||
type PageSearchParams = Promise<ResolvedSearchParams>;
|
||||
@@ -74,9 +76,15 @@ export default async function Page({ searchParams }: PageProps) {
|
||||
payerRows: filterSources.payerRows,
|
||||
});
|
||||
|
||||
const logoMappings = await prefetchLogoMappings(
|
||||
userId,
|
||||
transactionData.map((t) => t.name),
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
<MonthNavigation />
|
||||
<LogoPrefetchProvider mappings={logoMappings}>
|
||||
<TransactionsPage
|
||||
currentUserId={userId}
|
||||
transactions={transactionData}
|
||||
@@ -106,6 +114,7 @@ export default async function Page({ searchParams }: PageProps) {
|
||||
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
|
||||
attachmentMaxSizeMb={userPreferences?.attachmentMaxSizeMb ?? 50}
|
||||
/>
|
||||
</LogoPrefetchProvider>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
28
src/features/dashboard/extract-logo-names.ts
Normal file
28
src/features/dashboard/extract-logo-names.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { DashboardData } from "./fetch-dashboard-data";
|
||||
|
||||
/**
|
||||
* Coleta todos os nomes de estabelecimentos exibidos nos widgets do
|
||||
* dashboard que renderizam `<EstablishmentLogo />`. Usado para
|
||||
* pré-resolver os mapeamentos Logo.dev no servidor.
|
||||
*/
|
||||
export function extractDashboardLogoNames(data: DashboardData): string[] {
|
||||
const names: string[] = [];
|
||||
|
||||
for (const bill of data.billsSnapshot.bills) names.push(bill.name);
|
||||
for (const expense of data.recurringExpensesData.expenses)
|
||||
names.push(expense.name);
|
||||
for (const expense of data.installmentExpensesData.expenses)
|
||||
names.push(expense.name);
|
||||
for (const establishment of data.topEstablishmentsData.establishments)
|
||||
names.push(establishment.name);
|
||||
for (const expense of data.topExpensesAll.expenses) names.push(expense.name);
|
||||
for (const expense of data.topExpensesCardOnly.expenses)
|
||||
names.push(expense.name);
|
||||
for (const transactions of Object.values(
|
||||
data.purchasesByCategoryData.transactionsByCategory,
|
||||
)) {
|
||||
for (const transaction of transactions) names.push(transaction.name);
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
@@ -5,3 +5,4 @@ export type {
|
||||
export { CategoryIconBadge } from "./category-icon-badge";
|
||||
export { EstablishmentLogo } from "./establishment-logo";
|
||||
export { EstablishmentLogoPicker } from "./establishment-logo-picker";
|
||||
export { LogoPrefetchProvider } from "./logo-prefetch-provider";
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { type ReactNode, useRef } from "react";
|
||||
import { logoQueryKeys } from "@/shared/lib/logo";
|
||||
import type { LogoPrefetchEntry } from "@/shared/lib/logo/types";
|
||||
|
||||
type LogoPrefetchProviderProps = {
|
||||
mappings: LogoPrefetchEntry[];
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Semeia o cache do React Query com mapeamentos de logo já resolvidos
|
||||
* no servidor. Evita que cada `EstablishmentLogo` dispare seu próprio
|
||||
* GET para `/api/logo/mapping` no primeiro render.
|
||||
*/
|
||||
export function LogoPrefetchProvider({
|
||||
mappings,
|
||||
children,
|
||||
}: LogoPrefetchProviderProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const seeded = useRef(false);
|
||||
|
||||
if (!seeded.current) {
|
||||
for (const { nameKey, domain, logoUrl } of mappings) {
|
||||
queryClient.setQueryData(logoQueryKeys.mapping(nameKey), {
|
||||
domain,
|
||||
logoUrl,
|
||||
});
|
||||
}
|
||||
seeded.current = true;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
31
src/shared/lib/logo/prefetch-server.ts
Normal file
31
src/shared/lib/logo/prefetch-server.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import "server-only";
|
||||
|
||||
import { fetchEstablishmentLogoMap } from "./establishment-logo-queries";
|
||||
import { toNameKey } from "./index";
|
||||
import { buildLogoDevUrl } from "./server";
|
||||
import type { LogoPrefetchEntry } from "./types";
|
||||
|
||||
export async function prefetchLogoMappings(
|
||||
userId: string,
|
||||
names: string[],
|
||||
): Promise<LogoPrefetchEntry[]> {
|
||||
const uniqueNames = [
|
||||
...new Set(
|
||||
names.filter((n) => typeof n === "string" && n.trim().length > 0),
|
||||
),
|
||||
];
|
||||
if (uniqueNames.length === 0) return [];
|
||||
|
||||
const map = await fetchEstablishmentLogoMap(userId, uniqueNames);
|
||||
|
||||
const seen = new Set<string>();
|
||||
const entries: LogoPrefetchEntry[] = [];
|
||||
for (const name of uniqueNames) {
|
||||
const nameKey = toNameKey(name);
|
||||
if (seen.has(nameKey)) continue;
|
||||
seen.add(nameKey);
|
||||
const domain = map.get(nameKey) ?? null;
|
||||
entries.push({ nameKey, domain, logoUrl: buildLogoDevUrl(domain) });
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
5
src/shared/lib/logo/types.ts
Normal file
5
src/shared/lib/logo/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type LogoPrefetchEntry = {
|
||||
nameKey: string;
|
||||
domain: string | null;
|
||||
logoUrl: string | null;
|
||||
};
|
||||
Reference in New Issue
Block a user