refactor: migrate from ESLint to Biome and extract SQL queries to data.ts

- Replace ESLint with Biome for linting and formatting
- Configure Biome with tabs, double quotes, and organized imports
- Move all SQL/Drizzle queries from page.tsx files to data.ts files
- Create new data.ts files for: ajustes, dashboard, relatorios/categorias
- Update existing data.ts files: extrato, fatura (add lancamentos queries)
- Remove all drizzle-orm imports from page.tsx files
- Update README.md with new tooling info

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-01-27 13:15:37 +00:00
parent 8ffe61c59b
commit a7f63fb77a
442 changed files with 66141 additions and 69292 deletions

View File

@@ -1,46 +1,46 @@
"use server";
import { categorias, orcamentos } from "@/db/schema";
import {
type ActionResult,
handleActionError,
revalidateForEntity,
} from "@/lib/actions/helpers";
import { db } from "@/lib/db";
import { getUser } from "@/lib/auth/server";
import { periodSchema, uuidSchema } from "@/lib/schemas/common";
import {
formatDecimalForDbRequired,
normalizeDecimalInput,
} from "@/lib/utils/currency";
import { and, eq, ne } from "drizzle-orm";
import { z } from "zod";
import { categorias, orcamentos } from "@/db/schema";
import {
type ActionResult,
handleActionError,
revalidateForEntity,
} from "@/lib/actions/helpers";
import { getUser } from "@/lib/auth/server";
import { db } from "@/lib/db";
import { periodSchema, uuidSchema } from "@/lib/schemas/common";
import {
formatDecimalForDbRequired,
normalizeDecimalInput,
} from "@/lib/utils/currency";
const budgetBaseSchema = z.object({
categoriaId: uuidSchema("Categoria"),
period: periodSchema,
amount: z
.string({ message: "Informe o valor limite." })
.trim()
.min(1, "Informe o valor limite.")
.transform((value) => normalizeDecimalInput(value))
.refine(
(value) => !Number.isNaN(Number.parseFloat(value)),
"Informe um valor limite válido."
)
.transform((value) => Number.parseFloat(value))
.refine(
(value) => value >= 0,
"O valor limite deve ser maior ou igual a zero."
),
categoriaId: uuidSchema("Categoria"),
period: periodSchema,
amount: z
.string({ message: "Informe o valor limite." })
.trim()
.min(1, "Informe o valor limite.")
.transform((value) => normalizeDecimalInput(value))
.refine(
(value) => !Number.isNaN(Number.parseFloat(value)),
"Informe um valor limite válido.",
)
.transform((value) => Number.parseFloat(value))
.refine(
(value) => value >= 0,
"O valor limite deve ser maior ou igual a zero.",
),
});
const createBudgetSchema = budgetBaseSchema;
const updateBudgetSchema = budgetBaseSchema.extend({
id: uuidSchema("Orçamento"),
id: uuidSchema("Orçamento"),
});
const deleteBudgetSchema = z.object({
id: uuidSchema("Orçamento"),
id: uuidSchema("Orçamento"),
});
type BudgetCreateInput = z.infer<typeof createBudgetSchema>;
@@ -48,229 +48,227 @@ type BudgetUpdateInput = z.infer<typeof updateBudgetSchema>;
type BudgetDeleteInput = z.infer<typeof deleteBudgetSchema>;
const ensureCategory = async (userId: string, categoriaId: string) => {
const category = await db.query.categorias.findFirst({
columns: {
id: true,
type: true,
},
where: and(eq(categorias.id, categoriaId), eq(categorias.userId, userId)),
});
const category = await db.query.categorias.findFirst({
columns: {
id: true,
type: true,
},
where: and(eq(categorias.id, categoriaId), eq(categorias.userId, userId)),
});
if (!category) {
throw new Error("Categoria não encontrada.");
}
if (!category) {
throw new Error("Categoria não encontrada.");
}
if (category.type !== "despesa") {
throw new Error("Selecione uma categoria de despesa.");
}
if (category.type !== "despesa") {
throw new Error("Selecione uma categoria de despesa.");
}
};
export async function createBudgetAction(
input: BudgetCreateInput
input: BudgetCreateInput,
): Promise<ActionResult> {
try {
const user = await getUser();
const data = createBudgetSchema.parse(input);
try {
const user = await getUser();
const data = createBudgetSchema.parse(input);
await ensureCategory(user.id, data.categoriaId);
await ensureCategory(user.id, data.categoriaId);
const duplicateConditions = [
eq(orcamentos.userId, user.id),
eq(orcamentos.period, data.period),
eq(orcamentos.categoriaId, data.categoriaId),
] as const;
const duplicateConditions = [
eq(orcamentos.userId, user.id),
eq(orcamentos.period, data.period),
eq(orcamentos.categoriaId, data.categoriaId),
] as const;
const duplicate = await db.query.orcamentos.findFirst({
columns: { id: true },
where: and(...duplicateConditions),
});
const duplicate = await db.query.orcamentos.findFirst({
columns: { id: true },
where: and(...duplicateConditions),
});
if (duplicate) {
return {
success: false,
error:
"Já existe um orçamento para esta categoria no período selecionado.",
};
}
if (duplicate) {
return {
success: false,
error:
"Já existe um orçamento para esta categoria no período selecionado.",
};
}
await db.insert(orcamentos).values({
amount: formatDecimalForDbRequired(data.amount),
period: data.period,
userId: user.id,
categoriaId: data.categoriaId,
});
await db.insert(orcamentos).values({
amount: formatDecimalForDbRequired(data.amount),
period: data.period,
userId: user.id,
categoriaId: data.categoriaId,
});
revalidateForEntity("orcamentos");
revalidateForEntity("orcamentos");
return { success: true, message: "Orçamento criado com sucesso." };
} catch (error) {
return handleActionError(error);
}
return { success: true, message: "Orçamento criado com sucesso." };
} catch (error) {
return handleActionError(error);
}
}
export async function updateBudgetAction(
input: BudgetUpdateInput
input: BudgetUpdateInput,
): Promise<ActionResult> {
try {
const user = await getUser();
const data = updateBudgetSchema.parse(input);
try {
const user = await getUser();
const data = updateBudgetSchema.parse(input);
await ensureCategory(user.id, data.categoriaId);
await ensureCategory(user.id, data.categoriaId);
const duplicateConditions = [
eq(orcamentos.userId, user.id),
eq(orcamentos.period, data.period),
eq(orcamentos.categoriaId, data.categoriaId),
ne(orcamentos.id, data.id),
] as const;
const duplicateConditions = [
eq(orcamentos.userId, user.id),
eq(orcamentos.period, data.period),
eq(orcamentos.categoriaId, data.categoriaId),
ne(orcamentos.id, data.id),
] as const;
const duplicate = await db.query.orcamentos.findFirst({
columns: { id: true },
where: and(...duplicateConditions),
});
const duplicate = await db.query.orcamentos.findFirst({
columns: { id: true },
where: and(...duplicateConditions),
});
if (duplicate) {
return {
success: false,
error:
"Já existe um orçamento para esta categoria no período selecionado.",
};
}
if (duplicate) {
return {
success: false,
error:
"Já existe um orçamento para esta categoria no período selecionado.",
};
}
const [updated] = await db
.update(orcamentos)
.set({
amount: formatDecimalForDbRequired(data.amount),
period: data.period,
categoriaId: data.categoriaId,
})
.where(and(eq(orcamentos.id, data.id), eq(orcamentos.userId, user.id)))
.returning({ id: orcamentos.id });
const [updated] = await db
.update(orcamentos)
.set({
amount: formatDecimalForDbRequired(data.amount),
period: data.period,
categoriaId: data.categoriaId,
})
.where(and(eq(orcamentos.id, data.id), eq(orcamentos.userId, user.id)))
.returning({ id: orcamentos.id });
if (!updated) {
return {
success: false,
error: "Orçamento não encontrado.",
};
}
if (!updated) {
return {
success: false,
error: "Orçamento não encontrado.",
};
}
revalidateForEntity("orcamentos");
revalidateForEntity("orcamentos");
return { success: true, message: "Orçamento atualizado com sucesso." };
} catch (error) {
return handleActionError(error);
}
return { success: true, message: "Orçamento atualizado com sucesso." };
} catch (error) {
return handleActionError(error);
}
}
export async function deleteBudgetAction(
input: BudgetDeleteInput
input: BudgetDeleteInput,
): Promise<ActionResult> {
try {
const user = await getUser();
const data = deleteBudgetSchema.parse(input);
try {
const user = await getUser();
const data = deleteBudgetSchema.parse(input);
const [deleted] = await db
.delete(orcamentos)
.where(and(eq(orcamentos.id, data.id), eq(orcamentos.userId, user.id)))
.returning({ id: orcamentos.id });
const [deleted] = await db
.delete(orcamentos)
.where(and(eq(orcamentos.id, data.id), eq(orcamentos.userId, user.id)))
.returning({ id: orcamentos.id });
if (!deleted) {
return {
success: false,
error: "Orçamento não encontrado.",
};
}
if (!deleted) {
return {
success: false,
error: "Orçamento não encontrado.",
};
}
revalidateForEntity("orcamentos");
revalidateForEntity("orcamentos");
return { success: true, message: "Orçamento removido com sucesso." };
} catch (error) {
return handleActionError(error);
}
return { success: true, message: "Orçamento removido com sucesso." };
} catch (error) {
return handleActionError(error);
}
}
const duplicatePreviousMonthSchema = z.object({
period: periodSchema,
period: periodSchema,
});
type DuplicatePreviousMonthInput = z.infer<
typeof duplicatePreviousMonthSchema
>;
type DuplicatePreviousMonthInput = z.infer<typeof duplicatePreviousMonthSchema>;
export async function duplicatePreviousMonthBudgetsAction(
input: DuplicatePreviousMonthInput
input: DuplicatePreviousMonthInput,
): Promise<ActionResult> {
try {
const user = await getUser();
const data = duplicatePreviousMonthSchema.parse(input);
try {
const user = await getUser();
const data = duplicatePreviousMonthSchema.parse(input);
// Calcular mês anterior
const [year, month] = data.period.split("-").map(Number);
const currentDate = new Date(year, month - 1, 1);
const previousDate = new Date(currentDate);
previousDate.setMonth(previousDate.getMonth() - 1);
// Calcular mês anterior
const [year, month] = data.period.split("-").map(Number);
const currentDate = new Date(year, month - 1, 1);
const previousDate = new Date(currentDate);
previousDate.setMonth(previousDate.getMonth() - 1);
const prevYear = previousDate.getFullYear();
const prevMonth = String(previousDate.getMonth() + 1).padStart(2, "0");
const previousPeriod = `${prevYear}-${prevMonth}`;
const prevYear = previousDate.getFullYear();
const prevMonth = String(previousDate.getMonth() + 1).padStart(2, "0");
const previousPeriod = `${prevYear}-${prevMonth}`;
// Buscar orçamentos do mês anterior
const previousBudgets = await db.query.orcamentos.findMany({
where: and(
eq(orcamentos.userId, user.id),
eq(orcamentos.period, previousPeriod)
),
});
// Buscar orçamentos do mês anterior
const previousBudgets = await db.query.orcamentos.findMany({
where: and(
eq(orcamentos.userId, user.id),
eq(orcamentos.period, previousPeriod),
),
});
if (previousBudgets.length === 0) {
return {
success: false,
error: "Não foram encontrados orçamentos no mês anterior.",
};
}
if (previousBudgets.length === 0) {
return {
success: false,
error: "Não foram encontrados orçamentos no mês anterior.",
};
}
// Buscar orçamentos existentes do mês atual
const currentBudgets = await db.query.orcamentos.findMany({
where: and(
eq(orcamentos.userId, user.id),
eq(orcamentos.period, data.period)
),
});
// Buscar orçamentos existentes do mês atual
const currentBudgets = await db.query.orcamentos.findMany({
where: and(
eq(orcamentos.userId, user.id),
eq(orcamentos.period, data.period),
),
});
// Filtrar para evitar duplicatas
const existingCategoryIds = new Set(
currentBudgets.map((b) => b.categoriaId)
);
// Filtrar para evitar duplicatas
const existingCategoryIds = new Set(
currentBudgets.map((b) => b.categoriaId),
);
const budgetsToCopy = previousBudgets.filter(
(b) => b.categoriaId && !existingCategoryIds.has(b.categoriaId)
);
const budgetsToCopy = previousBudgets.filter(
(b) => b.categoriaId && !existingCategoryIds.has(b.categoriaId),
);
if (budgetsToCopy.length === 0) {
return {
success: false,
error:
"Todas as categorias do mês anterior já possuem orçamento neste mês.",
};
}
if (budgetsToCopy.length === 0) {
return {
success: false,
error:
"Todas as categorias do mês anterior já possuem orçamento neste mês.",
};
}
// Inserir novos orçamentos
await db.insert(orcamentos).values(
budgetsToCopy.map((b) => ({
amount: b.amount,
period: data.period,
userId: user.id,
categoriaId: b.categoriaId!,
}))
);
// Inserir novos orçamentos
await db.insert(orcamentos).values(
budgetsToCopy.map((b) => ({
amount: b.amount,
period: data.period,
userId: user.id,
categoriaId: b.categoriaId!,
})),
);
revalidateForEntity("orcamentos");
revalidateForEntity("orcamentos");
return {
success: true,
message: `${budgetsToCopy.length} orçamento${budgetsToCopy.length > 1 ? "s" : ""} duplicado${budgetsToCopy.length > 1 ? "s" : ""} com sucesso.`,
};
} catch (error) {
return handleActionError(error);
}
return {
success: true,
message: `${budgetsToCopy.length} orçamento${budgetsToCopy.length > 1 ? "s" : ""} duplicado${budgetsToCopy.length > 1 ? "s" : ""} com sucesso.`,
};
} catch (error) {
return handleActionError(error);
}
}

View File

@@ -1,125 +1,127 @@
import { and, asc, eq, inArray, sum } from "drizzle-orm";
import {
categorias,
lancamentos,
orcamentos,
type Orcamento,
categorias,
lancamentos,
type Orcamento,
orcamentos,
} from "@/db/schema";
import { db } from "@/lib/db";
import { and, asc, eq, inArray, sum } from "drizzle-orm";
const toNumber = (value: string | number | null | undefined) => {
if (typeof value === "number") return value;
if (typeof value === "string") {
const parsed = Number.parseFloat(value);
return Number.isNaN(parsed) ? 0 : parsed;
}
return 0;
if (typeof value === "number") return value;
if (typeof value === "string") {
const parsed = Number.parseFloat(value);
return Number.isNaN(parsed) ? 0 : parsed;
}
return 0;
};
export type BudgetData = {
id: string;
amount: number;
spent: number;
period: string;
createdAt: string;
category: {
id: string;
name: string;
icon: string | null;
} | null;
id: string;
amount: number;
spent: number;
period: string;
createdAt: string;
category: {
id: string;
name: string;
icon: string | null;
} | null;
};
export type CategoryOption = {
id: string;
name: string;
icon: string | null;
id: string;
name: string;
icon: string | null;
};
export async function fetchBudgetsForUser(
userId: string,
selectedPeriod: string
userId: string,
selectedPeriod: string,
): Promise<{
budgets: BudgetData[];
categoriesOptions: CategoryOption[];
budgets: BudgetData[];
categoriesOptions: CategoryOption[];
}> {
const [budgetRows, categoryRows] = await Promise.all([
db.query.orcamentos.findMany({
where: and(
eq(orcamentos.userId, userId),
eq(orcamentos.period, selectedPeriod)
),
with: {
categoria: true,
},
}),
db.query.categorias.findMany({
columns: {
id: true,
name: true,
icon: true,
},
where: and(eq(categorias.userId, userId), eq(categorias.type, "despesa")),
orderBy: asc(categorias.name),
}),
]);
const [budgetRows, categoryRows] = await Promise.all([
db.query.orcamentos.findMany({
where: and(
eq(orcamentos.userId, userId),
eq(orcamentos.period, selectedPeriod),
),
with: {
categoria: true,
},
}),
db.query.categorias.findMany({
columns: {
id: true,
name: true,
icon: true,
},
where: and(eq(categorias.userId, userId), eq(categorias.type, "despesa")),
orderBy: asc(categorias.name),
}),
]);
const categoryIds = budgetRows
.map((budget: Orcamento) => budget.categoriaId)
.filter((id: string | null): id is string => Boolean(id));
const categoryIds = budgetRows
.map((budget: Orcamento) => budget.categoriaId)
.filter((id: string | null): id is string => Boolean(id));
let totalsByCategory = new Map<string, number>();
let totalsByCategory = new Map<string, number>();
if (categoryIds.length > 0) {
const totals = await db
.select({
categoriaId: lancamentos.categoriaId,
totalAmount: sum(lancamentos.amount).as("totalAmount"),
})
.from(lancamentos)
.where(
and(
eq(lancamentos.userId, userId),
eq(lancamentos.period, selectedPeriod),
eq(lancamentos.transactionType, "Despesa"),
inArray(lancamentos.categoriaId, categoryIds)
)
)
.groupBy(lancamentos.categoriaId);
if (categoryIds.length > 0) {
const totals = await db
.select({
categoriaId: lancamentos.categoriaId,
totalAmount: sum(lancamentos.amount).as("totalAmount"),
})
.from(lancamentos)
.where(
and(
eq(lancamentos.userId, userId),
eq(lancamentos.period, selectedPeriod),
eq(lancamentos.transactionType, "Despesa"),
inArray(lancamentos.categoriaId, categoryIds),
),
)
.groupBy(lancamentos.categoriaId);
totalsByCategory = new Map(
totals.map((row: { categoriaId: string | null; totalAmount: string | null }) => [
row.categoriaId ?? "",
Math.abs(toNumber(row.totalAmount)),
])
);
}
totalsByCategory = new Map(
totals.map(
(row: { categoriaId: string | null; totalAmount: string | null }) => [
row.categoriaId ?? "",
Math.abs(toNumber(row.totalAmount)),
],
),
);
}
const budgets = budgetRows
.map((budget: Orcamento) => ({
id: budget.id,
amount: toNumber(budget.amount),
spent: totalsByCategory.get(budget.categoriaId ?? "") ?? 0,
period: budget.period,
createdAt: budget.createdAt.toISOString(),
category: budget.categoria
? {
id: budget.categoria.id,
name: budget.categoria.name,
icon: budget.categoria.icon,
}
: null,
}))
.sort((a, b) =>
(a.category?.name ?? "").localeCompare(b.category?.name ?? "", "pt-BR", {
sensitivity: "base",
})
);
const budgets = budgetRows
.map((budget: Orcamento) => ({
id: budget.id,
amount: toNumber(budget.amount),
spent: totalsByCategory.get(budget.categoriaId ?? "") ?? 0,
period: budget.period,
createdAt: budget.createdAt.toISOString(),
category: budget.categoria
? {
id: budget.categoria.id,
name: budget.categoria.name,
icon: budget.categoria.icon,
}
: null,
}))
.sort((a, b) =>
(a.category?.name ?? "").localeCompare(b.category?.name ?? "", "pt-BR", {
sensitivity: "base",
}),
);
const categoriesOptions = categoryRows.map((category) => ({
id: category.id,
name: category.name,
icon: category.icon,
}));
const categoriesOptions = categoryRows.map((category) => ({
id: category.id,
name: category.name,
icon: category.icon,
}));
return { budgets, categoriesOptions };
return { budgets, categoriesOptions };
}

View File

@@ -1,23 +1,23 @@
import PageDescription from "@/components/page-description";
import { RiFundsLine } from "@remixicon/react";
import PageDescription from "@/components/page-description";
export const metadata = {
title: "Anotações | Opensheets",
title: "Anotações | Opensheets",
};
export default function RootLayout({
children,
children,
}: {
children: React.ReactNode;
children: React.ReactNode;
}) {
return (
<section className="space-y-6 px-6">
<PageDescription
icon={<RiFundsLine />}
title="Orçamentos"
subtitle="Gerencie seus orçamentos mensais por categorias. Acompanhe o progresso do seu orçamento e faça ajustes conforme necessário."
/>
{children}
</section>
);
return (
<section className="space-y-6 px-6">
<PageDescription
icon={<RiFundsLine />}
title="Orçamentos"
subtitle="Gerencie seus orçamentos mensais por categorias. Acompanhe o progresso do seu orçamento e faça ajustes conforme necessário."
/>
{children}
</section>
);
}

View File

@@ -5,64 +5,61 @@ import { Skeleton } from "@/components/ui/skeleton";
* Layout: MonthPicker + Header + Grid de cards de orçamento
*/
export default function OrcamentosLoading() {
return (
<main className="flex flex-col gap-6">
{/* Month Picker placeholder */}
<div className="h-[60px] animate-pulse rounded-2xl bg-foreground/10" />
return (
<main className="flex flex-col gap-6">
{/* Month Picker placeholder */}
<div className="h-[60px] animate-pulse rounded-2xl bg-foreground/10" />
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="space-y-2">
<Skeleton className="h-8 w-48 rounded-2xl bg-foreground/10" />
<Skeleton className="h-5 w-64 rounded-2xl bg-foreground/10" />
</div>
<Skeleton className="h-10 w-40 rounded-2xl bg-foreground/10" />
</div>
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="space-y-2">
<Skeleton className="h-8 w-48 rounded-2xl bg-foreground/10" />
<Skeleton className="h-5 w-64 rounded-2xl bg-foreground/10" />
</div>
<Skeleton className="h-10 w-40 rounded-2xl bg-foreground/10" />
</div>
{/* Grid de cards de orçamentos */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
className="rounded-2xl border p-6 space-y-4"
>
{/* Categoria com ícone */}
<div className="flex items-center gap-3">
<Skeleton className="size-10 rounded-2xl bg-foreground/10" />
<div className="flex-1 space-y-2">
<Skeleton className="h-5 w-32 rounded-2xl bg-foreground/10" />
<Skeleton className="h-4 w-20 rounded-2xl bg-foreground/10" />
</div>
</div>
{/* Grid de cards de orçamentos */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="rounded-2xl border p-6 space-y-4">
{/* Categoria com ícone */}
<div className="flex items-center gap-3">
<Skeleton className="size-10 rounded-2xl bg-foreground/10" />
<div className="flex-1 space-y-2">
<Skeleton className="h-5 w-32 rounded-2xl bg-foreground/10" />
<Skeleton className="h-4 w-20 rounded-2xl bg-foreground/10" />
</div>
</div>
{/* Valor orçado */}
<div className="space-y-2 pt-4 border-t">
<Skeleton className="h-4 w-24 rounded-2xl bg-foreground/10" />
<Skeleton className="h-7 w-32 rounded-2xl bg-foreground/10" />
</div>
{/* Valor orçado */}
<div className="space-y-2 pt-4 border-t">
<Skeleton className="h-4 w-24 rounded-2xl bg-foreground/10" />
<Skeleton className="h-7 w-32 rounded-2xl bg-foreground/10" />
</div>
{/* Valor gasto */}
<div className="space-y-2">
<Skeleton className="h-4 w-20 rounded-2xl bg-foreground/10" />
<Skeleton className="h-6 w-28 rounded-2xl bg-foreground/10" />
</div>
{/* Valor gasto */}
<div className="space-y-2">
<Skeleton className="h-4 w-20 rounded-2xl bg-foreground/10" />
<Skeleton className="h-6 w-28 rounded-2xl bg-foreground/10" />
</div>
{/* Barra de progresso */}
<div className="space-y-2">
<Skeleton className="h-2 w-full rounded-full bg-foreground/10" />
<Skeleton className="h-3 w-16 rounded-2xl bg-foreground/10" />
</div>
{/* Barra de progresso */}
<div className="space-y-2">
<Skeleton className="h-2 w-full rounded-full bg-foreground/10" />
<Skeleton className="h-3 w-16 rounded-2xl bg-foreground/10" />
</div>
{/* Botões de ação */}
<div className="flex gap-2 pt-2">
<Skeleton className="h-9 flex-1 rounded-2xl bg-foreground/10" />
<Skeleton className="h-9 w-9 rounded-2xl bg-foreground/10" />
</div>
</div>
))}
</div>
</div>
</main>
);
{/* Botões de ação */}
<div className="flex gap-2 pt-2">
<Skeleton className="h-9 flex-1 rounded-2xl bg-foreground/10" />
<Skeleton className="h-9 w-9 rounded-2xl bg-foreground/10" />
</div>
</div>
))}
</div>
</div>
</main>
);
}

View File

@@ -7,45 +7,48 @@ import { fetchBudgetsForUser } from "./data";
type PageSearchParams = Promise<Record<string, string | string[] | undefined>>;
type PageProps = {
searchParams?: PageSearchParams;
searchParams?: PageSearchParams;
};
const getSingleParam = (
params: Record<string, string | string[] | undefined> | undefined,
key: string
params: Record<string, string | string[] | undefined> | undefined,
key: string,
) => {
const value = params?.[key];
if (!value) return null;
return Array.isArray(value) ? value[0] ?? null : value;
const value = params?.[key];
if (!value) return null;
return Array.isArray(value) ? (value[0] ?? null) : value;
};
const capitalize = (value: string) =>
value.length === 0 ? value : value[0]?.toUpperCase() + value.slice(1);
value.length === 0 ? value : value[0]?.toUpperCase() + value.slice(1);
export default async function Page({ searchParams }: PageProps) {
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
const {
period: selectedPeriod,
monthName: rawMonthName,
year,
} = parsePeriodParam(periodoParam);
const {
period: selectedPeriod,
monthName: rawMonthName,
year,
} = parsePeriodParam(periodoParam);
const periodLabel = `${capitalize(rawMonthName)} ${year}`;
const periodLabel = `${capitalize(rawMonthName)} ${year}`;
const { budgets, categoriesOptions } = await fetchBudgetsForUser(userId, selectedPeriod);
const { budgets, categoriesOptions } = await fetchBudgetsForUser(
userId,
selectedPeriod,
);
return (
<main className="flex flex-col gap-6">
<MonthNavigation />
<BudgetsPage
budgets={budgets}
categories={categoriesOptions}
selectedPeriod={selectedPeriod}
periodLabel={periodLabel}
/>
</main>
);
return (
<main className="flex flex-col gap-6">
<MonthNavigation />
<BudgetsPage
budgets={budgets}
categories={categoriesOptions}
selectedPeriod={selectedPeriod}
periodLabel={periodLabel}
/>
</main>
);
}