diff --git a/app/(auth)/budgets/edit/[id]/loading.tsx b/app/(auth)/budgets/edit/[id]/loading.tsx
new file mode 100644
index 0000000..8c4aa13
--- /dev/null
+++ b/app/(auth)/budgets/edit/[id]/loading.tsx
@@ -0,0 +1,5 @@
+import FormSkeleton from "@/components/skeletons/form";
+
+export default function Loading() {
+ return ;
+}
diff --git a/app/(auth)/budgets/page.tsx b/app/(auth)/budgets/page.tsx
index 046d080..c8e0fa2 100644
--- a/app/(auth)/budgets/page.tsx
+++ b/app/(auth)/budgets/page.tsx
@@ -1,40 +1,24 @@
import AddButton from "@/components/add-button";
-import BudgetsList from "@/components/budgets/list";
-import EmptyList from "@/components/empty-list";
+import { BudgetsContainer } from "@/components/budgets/container";
import PageTitle from "@/components/page-title";
-import { Card, CardContent } from "@/components/ui/card";
-import { getBudgets, getExpensesByCategory } from "@/lib/dal";
-import { getCurrentMonthRange } from "@/lib/utils";
+import BudgetsSkeleton from "@/components/skeletons/budgets";
import type { Metadata } from "next";
+import { Suspense } from "react";
export const metadata: Metadata = {
title: "Budgets"
};
export default async function BudgetsPage() {
- const currentDate = new Date();
- const monthRange = getCurrentMonthRange(currentDate);
-
- const [budgets, expenses] = await Promise.all([
- getBudgets(),
- getExpensesByCategory(monthRange.start, monthRange.end)
- ]);
-
return (
<>
- {budgets.length === 0 ? (
-
-
-
-
-
- ) : (
-
- )}
+ }>
+
+
>
);
}
diff --git a/app/(auth)/dashboard/page.tsx b/app/(auth)/dashboard/page.tsx
index 84a7202..2fe408e 100644
--- a/app/(auth)/dashboard/page.tsx
+++ b/app/(auth)/dashboard/page.tsx
@@ -1,22 +1,11 @@
import { Card, CardContent, CardHeader } from "@/components/ui/card";
-import { getExpensesByDateRange, getFirstExpense } from "@/lib/dal";
-import {
- buildChartData,
- calculateTotalAmount,
- createDateFromDay,
- getCurrentMonthRange,
- parseSelectedDay,
- parseSelectedMonth
-} from "@/lib/utils";
-import FormattedAmount from "@/components/formatted-amount";
import ExpensesList from "@/components/expenses/list";
-import DailyBarChart from "@/components/daily-bar-chart";
-import { format, getDate } from "date-fns";
-import MonthSelect from "@/components/month-select";
-import { filterExpensesByDay } from "@/lib/utils";
+import Chart from "@/components/dashboard/chart";
import type { Metadata } from "next";
import EmptyList from "@/components/empty-list";
import PageTitle from "@/components/page-title";
+import ChartHeader from "@/components/dashboard/chart-header";
+import { getDashboardData } from "@/lib/dashboard-data";
export const metadata: Metadata = {
title: "Dashboard"
@@ -25,60 +14,33 @@ export const metadata: Metadata = {
export default async function Page({
searchParams
}: {
- searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+ searchParams: Promise<{ day?: string; month?: string }>;
}) {
const params = await searchParams;
- const currentDate = parseSelectedMonth(params.month) || new Date();
- const monthRange = getCurrentMonthRange(currentDate);
- const selectedDayParam = parseSelectedDay(params.day);
- const highlightedDate = selectedDayParam
- ? createDateFromDay(selectedDayParam, currentDate)
- : null;
-
- const [monthlyExpenses, firstExpense] = await Promise.all([
- getExpensesByDateRange(monthRange.start, currentDate),
- getFirstExpense()
- ]);
-
- const totalSpent = calculateTotalAmount(monthlyExpenses);
- let selectedExpenses = monthlyExpenses;
- let amountSpent = totalSpent / getDate(currentDate);
-
- if (selectedDayParam) {
- const dayExpenses = filterExpensesByDay(monthlyExpenses, selectedDayParam);
- const dayTotal = calculateTotalAmount(dayExpenses);
-
- selectedExpenses = dayExpenses ?? [];
- amountSpent = dayTotal ?? 0;
- }
-
- const chartData = buildChartData(monthlyExpenses, getDate(monthRange.end));
+ const {
+ totalSpent,
+ amountSpent,
+ selectedExpenses,
+ highlightedDate,
+ chartData,
+ monthSelectOptions
+ } = await getDashboardData(params.month, params.day);
return (
<>
-
-
-
-
-
-
- {highlightedDate === null
- ? "Spent/day"
- : format(highlightedDate, "d MMM yyyy")}
-
-
-
+
-
+
diff --git a/app/(auth)/expenses/edit/[id]/loading.tsx b/app/(auth)/expenses/edit/[id]/loading.tsx
new file mode 100644
index 0000000..8c4aa13
--- /dev/null
+++ b/app/(auth)/expenses/edit/[id]/loading.tsx
@@ -0,0 +1,5 @@
+import FormSkeleton from "@/components/skeletons/form";
+
+export default function Loading() {
+ return ;
+}
diff --git a/app/(auth)/expenses/page.tsx b/app/(auth)/expenses/page.tsx
index b974b01..447bd7f 100644
--- a/app/(auth)/expenses/page.tsx
+++ b/app/(auth)/expenses/page.tsx
@@ -1,8 +1,15 @@
import type { Metadata } from "next";
-import { notFound } from "next/navigation";
import PageTitle from "@/components/page-title";
import AddButton from "@/components/add-button";
-import PaginatedExpensesList from "@/components/expenses/paginated-list";
+import ExpensesListWrapper from "@/components/expenses/list-wrapper";
+import { Suspense } from "react";
+import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
+import SearchBar from "@/components/expenses/search-bar";
+import { getExpensesPages } from "@/lib/dal";
+import PaginationControls from "@/components/pagination-controls";
+import ExpensesSkeleton from "@/components/skeletons/expenses";
+
+const PAGE_SIZE = 15;
export const metadata: Metadata = {
title: "Expenses"
@@ -11,15 +18,13 @@ export const metadata: Metadata = {
export default async function Page({
searchParams
}: {
- searchParams: Promise<{ page: string; query: string }>;
+ searchParams: Promise<{ page?: string; query?: string }>;
}) {
const params = await searchParams;
- const page = params.page ? Number(params.page) : 1;
- const query = params.query;
+ const page = Number(params.page) || 1;
+ const query = params.query || "";
- if (isNaN(page) || page < 1) {
- notFound();
- }
+ const totalPages = await getExpensesPages(query, PAGE_SIZE);
return (
<>
@@ -27,7 +32,21 @@ export default async function Page({
-
+
+
+
+
+
+ }>
+
+
+
+ {totalPages > 1 && (
+
+
+
+ )}
+
>
);
}
diff --git a/components/budgets/container.tsx b/components/budgets/container.tsx
new file mode 100644
index 0000000..6ee5bed
--- /dev/null
+++ b/components/budgets/container.tsx
@@ -0,0 +1,29 @@
+import BudgetsList from "@/components/budgets/list";
+import EmptyList from "@/components/empty-list";
+import { Card, CardContent } from "@/components/ui/card";
+import { getBudgets, getExpensesByCategory } from "@/lib/dal";
+import { getCurrentMonthRange } from "@/lib/utils";
+
+export async function BudgetsContainer() {
+ const currentDate = new Date();
+ const monthRange = getCurrentMonthRange(currentDate);
+
+ const [budgets, expenses] = await Promise.all([
+ getBudgets(),
+ getExpensesByCategory(monthRange.start, monthRange.end)
+ ]);
+
+ return (
+ <>
+ {budgets.length === 0 ? (
+
+
+
+
+
+ ) : (
+
+ )}
+ >
+ );
+}
diff --git a/components/dashboard/chart-header.tsx b/components/dashboard/chart-header.tsx
new file mode 100644
index 0000000..ead1c73
--- /dev/null
+++ b/components/dashboard/chart-header.tsx
@@ -0,0 +1,36 @@
+import FormattedAmount from "@/components/formatted-amount";
+import { format } from "date-fns";
+import MonthSelect from "@/components/dashboard/month-select";
+
+type Props = {
+ totalSpent: number;
+ amountSpent: number;
+ highlightedDate: Date | null;
+ monthSelectOptions: { value: string; label: string }[];
+};
+
+export default async function ChartHeader({
+ totalSpent,
+ amountSpent,
+ highlightedDate,
+ monthSelectOptions
+}: Props) {
+ return (
+ <>
+
+
+
+
+
+
+ {highlightedDate === null ? "Spent/day" : format(highlightedDate, "d MMM yyyy")}
+
+
+
+ >
+ );
+}
diff --git a/components/daily-bar-chart.tsx b/components/dashboard/chart.tsx
similarity index 96%
rename from components/daily-bar-chart.tsx
rename to components/dashboard/chart.tsx
index 6f04f5c..f2e40e8 100644
--- a/components/daily-bar-chart.tsx
+++ b/components/dashboard/chart.tsx
@@ -17,7 +17,7 @@ function formatToK(num: number) {
return num.toString();
}
-export default function DailyBarChart({ chartData }: Props) {
+export default function Chart({ chartData }: Props) {
const searchParams = useSearchParams();
const router = useRouter();
diff --git a/components/month-select.tsx b/components/dashboard/month-select.tsx
similarity index 60%
rename from components/month-select.tsx
rename to components/dashboard/month-select.tsx
index 666f7f7..0aa9d80 100644
--- a/components/month-select.tsx
+++ b/components/dashboard/month-select.tsx
@@ -8,38 +8,22 @@ import {
SelectTrigger,
SelectValue
} from "@/components/ui/select";
-import { useEffect, useState } from "react";
-import { buildMonthSelectOptions } from "@/lib/utils";
-import { format } from "date-fns";
-const now = new Date();
-const defaultOptions = [
- {
- label: format(now, "MMM yyyy"),
- value: format(now, "yyyy-MM")
- }
-];
+type Props = {
+ options: {
+ value: string;
+ label: string;
+ }[];
+};
-export default function MonthSelect({ startDate }: { startDate?: Date }) {
- const [options, setOptions] =
- useState>(defaultOptions);
+export default function MonthSelect({ options }: Props) {
const searchParams = useSearchParams();
const router = useRouter();
const selectedMonth = searchParams.get("month");
- useEffect(() => {
- if (!startDate) {
- return;
- }
- const monthOptions = buildMonthSelectOptions(startDate);
- setOptions(monthOptions);
- }, [startDate]);
-
const handleChange = (value: string) => {
const params = new URLSearchParams();
- if (value !== defaultOptions[0].value) {
- params.set("month", value);
- }
+ params.set("month", value);
router.push(`?${params.toString()}`);
};
diff --git a/components/expenses/list-wrapper.tsx b/components/expenses/list-wrapper.tsx
new file mode 100644
index 0000000..d8cc2f2
--- /dev/null
+++ b/components/expenses/list-wrapper.tsx
@@ -0,0 +1,21 @@
+import { getPaginatedExpenses } from "@/lib/dal";
+import EmptyList from "@/components/empty-list";
+import ExpensesList from "@/components/expenses/list";
+
+export default async function ExpensesListWrapper({
+ query,
+ currentPage,
+ pageSize
+}: {
+ query?: string;
+ currentPage: number;
+ pageSize: number;
+}) {
+ const expenses = await getPaginatedExpenses(currentPage, pageSize, query);
+
+ if (expenses.length === 0) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/components/expenses/paginated-list.tsx b/components/expenses/paginated-list.tsx
deleted file mode 100644
index aa246b5..0000000
--- a/components/expenses/paginated-list.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { notFound } from "next/navigation";
-import { getPaginatedExpenses } from "@/lib/dal";
-import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
-import EmptyList from "@/components/empty-list";
-import ExpensesList from "@/components/expenses/list";
-import PaginationControls from "@/components/pagination-controls";
-import SearchBar from "@/components/expenses/search-bar";
-
-export default async function PaginatedExpensesList({
- query,
- page
-}: {
- query: string;
- page: number;
-}) {
- const expensesData = await getPaginatedExpenses(page, 15, query);
-
- const { expenses, totalPages, currentPage, hasNextPage, hasPreviousPage } =
- expensesData;
-
- if (currentPage > totalPages && totalPages > 0) {
- notFound();
- }
-
- return (
-
-
-
-
-
- {expenses.length === 0 ? : }
-
- {totalPages > 1 && (
-
-
-
- )}
-
- );
-}
diff --git a/components/pagination-controls.tsx b/components/pagination-controls.tsx
index 04da72a..3555b67 100644
--- a/components/pagination-controls.tsx
+++ b/components/pagination-controls.tsx
@@ -11,22 +11,17 @@ import {
PaginationPrevious
} from "@/components/ui/pagination";
-type PaginationControlsProps = {
- currentPage: number;
+type Props = {
totalPages: number;
- hasNextPage: boolean;
- hasPreviousPage: boolean;
};
-export default function PaginationControls({
- currentPage,
- totalPages,
- hasNextPage,
- hasPreviousPage
-}: PaginationControlsProps) {
+export default function PaginationControls({ totalPages }: Props) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
+ const currentPage = Number(searchParams.get("page")) || 1;
+ const hasNextPage = currentPage < totalPages;
+ const hasPreviousPage = currentPage > 1;
const createPageURL = (page: number) => {
const params = new URLSearchParams(searchParams.toString());
@@ -78,12 +73,10 @@ export default function PaginationControls({
{
- if (!hasPreviousPage) {
- e.preventDefault();
- return;
- }
e.preventDefault();
- navigateToPage(currentPage - 1);
+ if (hasPreviousPage) {
+ navigateToPage(currentPage - 1);
+ }
}}
aria-disabled={!hasPreviousPage}
className={!hasPreviousPage ? "pointer-events-none opacity-50" : ""}
@@ -113,12 +106,10 @@ export default function PaginationControls({
{
- if (!hasNextPage) {
- e.preventDefault();
- return;
- }
e.preventDefault();
- navigateToPage(currentPage + 1);
+ if (hasNextPage) {
+ navigateToPage(currentPage + 1);
+ }
}}
aria-disabled={!hasNextPage}
className={!hasNextPage ? "pointer-events-none opacity-50" : ""}
diff --git a/components/skeletons/budgets.tsx b/components/skeletons/budgets.tsx
new file mode 100644
index 0000000..df97905
--- /dev/null
+++ b/components/skeletons/budgets.tsx
@@ -0,0 +1,37 @@
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function BudgetsSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/skeletons/expenses.tsx b/components/skeletons/expenses.tsx
new file mode 100644
index 0000000..1b68d8b
--- /dev/null
+++ b/components/skeletons/expenses.tsx
@@ -0,0 +1,31 @@
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function ExpensesSkeleton() {
+ return (
+
+ );
+}
diff --git a/components/skeletons/form.tsx b/components/skeletons/form.tsx
new file mode 100644
index 0000000..fef1912
--- /dev/null
+++ b/components/skeletons/form.tsx
@@ -0,0 +1,46 @@
+import {
+ Card,
+ CardAction,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ CardTitle
+} from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function FormSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/lib/dal.ts b/lib/dal.ts
index b5463b7..08cfb7d 100644
--- a/lib/dal.ts
+++ b/lib/dal.ts
@@ -108,61 +108,54 @@ export async function getExpensesByCategory(
}
}
+export async function getExpensesPages(query: string, pageSize: number): Promise {
+ const session = await verifySession();
+
+ try {
+ const data = await prisma.expense.count({
+ where: {
+ userId: session.userId,
+ item: { startsWith: query, mode: "insensitive" }
+ }
+ });
+
+ return Math.ceil(data / pageSize);
+ } catch (error) {
+ throw error;
+ }
+}
export async function getPaginatedExpenses(
- page: number = 1,
- pageSize: number = 15,
- startsWith: string = ""
-): Promise<{
- expenses: ExpenseWithColor[];
- totalPages: number;
- currentPage: number;
- hasNextPage: boolean;
- hasPreviousPage: boolean;
-}> {
+ page: number,
+ pageSize: number,
+ query: string = ""
+): Promise {
const session = await verifySession();
try {
- const validPage = Math.max(1, page);
- const skip = (validPage - 1) * pageSize;
+ const skip = (page - 1) * pageSize;
- const [data, totalCount] = await Promise.all([
- prisma.expense.findMany({
- where: {
- userId: session.userId,
- item: { startsWith, mode: "insensitive" }
- },
- select: {
- id: true,
- item: true,
- value: true,
- category: {
- select: {
- color: true
- }
- },
- createdAt: true
+ const data = await prisma.expense.findMany({
+ where: {
+ userId: session.userId,
+ item: { startsWith: query, mode: "insensitive" }
+ },
+ select: {
+ id: true,
+ item: true,
+ value: true,
+ category: {
+ select: {
+ color: true
+ }
},
- orderBy: { createdAt: "desc" },
- skip,
- take: pageSize
- }),
- prisma.expense.count({
- where: {
- userId: session.userId,
- item: { startsWith }
- }
- })
- ]);
-
- const totalPages = Math.ceil(totalCount / pageSize);
+ createdAt: true
+ },
+ orderBy: { createdAt: "desc" },
+ skip,
+ take: pageSize
+ });
- return {
- expenses: data,
- totalPages,
- currentPage: validPage,
- hasNextPage: validPage < totalPages,
- hasPreviousPage: validPage > 1
- };
+ return data;
} catch (error) {
throw error;
}
diff --git a/lib/dashboard-data.ts b/lib/dashboard-data.ts
new file mode 100644
index 0000000..4b22c14
--- /dev/null
+++ b/lib/dashboard-data.ts
@@ -0,0 +1,50 @@
+import { getExpensesByDateRange, getFirstExpense } from "@/lib/dal";
+import {
+ parseSelectedMonth,
+ parseSelectedDay,
+ getCurrentMonthRange,
+ filterExpensesByDay,
+ calculateTotalAmount,
+ createDateFromDay,
+ buildChartData,
+ buildMonthSelectOptions
+} from "@/lib/utils";
+import { getDate } from "date-fns";
+
+export async function getDashboardData(month?: string, day?: string) {
+ const currentDate = parseSelectedMonth(month) || new Date();
+ const selectedDayParam = parseSelectedDay(day);
+ const monthRange = getCurrentMonthRange(currentDate);
+
+ const [monthlyExpenses, firstExpense] = await Promise.all([
+ getExpensesByDateRange(monthRange.start, currentDate),
+ getFirstExpense()
+ ]);
+
+ const totalSpent = calculateTotalAmount(monthlyExpenses);
+
+ const selectedExpenses = selectedDayParam
+ ? filterExpensesByDay(monthlyExpenses, selectedDayParam)
+ : monthlyExpenses;
+
+ const amountSpent = selectedDayParam
+ ? calculateTotalAmount(selectedExpenses)
+ : totalSpent / getDate(currentDate);
+
+ const highlightedDate = selectedDayParam
+ ? createDateFromDay(selectedDayParam, currentDate)
+ : null;
+
+ const monthSelectOptions = buildMonthSelectOptions(firstExpense?.createdAt);
+ const chartData = buildChartData(monthlyExpenses, getDate(monthRange.end));
+
+ return {
+ totalSpent,
+ amountSpent,
+ selectedExpenses,
+ highlightedDate,
+ chartData,
+ monthSelectOptions,
+ monthRange
+ };
+}
diff --git a/lib/utils/chart.ts b/lib/utils/chart.ts
index d3362b3..e606dc5 100644
--- a/lib/utils/chart.ts
+++ b/lib/utils/chart.ts
@@ -26,10 +26,10 @@ export function buildChartData(
return chartData;
}
-export function buildMonthSelectOptions(date: Date) {
+export function buildMonthSelectOptions(date?: Date) {
const now = new Date();
- const startYear = getYear(date);
- const startMonth = getMonth(date);
+ const startYear = getYear(date || now);
+ const startMonth = getMonth(date || now);
const endYear = getYear(now);
const endMonth = getMonth(now);
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..7a07296 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
+ reactCompiler: true
};
export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 0061adb..ea3ba06 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -53,6 +53,7 @@
"@types/react": "19",
"@types/react-dom": "19",
"@types/supertest": "6.0.3",
+ "babel-plugin-react-compiler": "^1.0.0",
"dotenv": "17.2.3",
"eslint": "9",
"eslint-config-next": "15.5.4",
@@ -85,6 +86,40 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@date-fns/tz": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
@@ -4643,6 +4678,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/babel-plugin-react-compiler": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
+ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.26.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
diff --git a/package.json b/package.json
index 38fb7a9..fe5e867 100644
--- a/package.json
+++ b/package.json
@@ -67,6 +67,7 @@
"@types/react": "19",
"@types/react-dom": "19",
"@types/supertest": "6.0.3",
+ "babel-plugin-react-compiler": "^1.0.0",
"dotenv": "17.2.3",
"eslint": "9",
"eslint-config-next": "15.5.4",
diff --git a/tests/integration/expenses.test.ts b/tests/integration/expenses.test.ts
index 441e4be..c674c64 100644
--- a/tests/integration/expenses.test.ts
+++ b/tests/integration/expenses.test.ts
@@ -4,7 +4,8 @@ import {
getExpenseById,
getPaginatedExpenses,
getExpensesByDateRange,
- getExpensesByCategory
+ getExpensesByCategory,
+ getExpensesPages
} from "@/lib/dal";
import { encrypt } from "@/lib/auth/session";
@@ -49,29 +50,23 @@ describe("expenses", () => {
});
mockGet.mockReturnValue({ value: token });
- const result = await getPaginatedExpenses();
+ const result = await getPaginatedExpenses(1, 5);
- expect(result).toMatchObject({
- currentPage: 1,
- expenses: [
- {
- item: "Fruits",
- value: 100,
- category: {
- color: foodCategory.color
- }
- },
- {
- item: "Meat",
- value: 50,
- category: {
- color: foodCategory.color
- }
- }
- ],
- hasNextPage: false,
- hasPreviousPage: false,
- totalPages: 1
+ expect(result).toHaveLength(2);
+
+ expect(result[0]).toMatchObject({
+ item: "Fruits",
+ value: 100,
+ category: {
+ color: foodCategory.color
+ }
+ });
+ expect(result[1]).toMatchObject({
+ item: "Meat",
+ value: 50,
+ category: {
+ color: foodCategory.color
+ }
});
});
@@ -106,28 +101,21 @@ describe("expenses", () => {
mockGet.mockReturnValue({ value: token });
const result = await getPaginatedExpenses(2, 5);
+ expect(result).toHaveLength(2);
- expect(result).toMatchObject({
- currentPage: 2,
- expenses: [
- {
- item: "Item6",
- value: 60,
- category: {
- color: foodCategory.color
- }
- },
- {
- item: "Item7",
- value: 70,
- category: {
- color: foodCategory.color
- }
- }
- ],
- hasNextPage: false,
- hasPreviousPage: true,
- totalPages: 2
+ expect(result[0]).toMatchObject({
+ item: "Item6",
+ value: 60,
+ category: {
+ color: foodCategory.color
+ }
+ });
+ expect(result[1]).toMatchObject({
+ item: "Item7",
+ value: 70,
+ category: {
+ color: foodCategory.color
+ }
});
});
@@ -165,14 +153,12 @@ describe("expenses", () => {
});
mockGet.mockReturnValue({ value: token });
- const result = await getPaginatedExpenses();
+ const result = await getPaginatedExpenses(1, 5);
- expect(result).toMatchObject({
- currentPage: 1,
- expenses: [{ item: "Item1", value: 100, category: { color: category1.color } }],
- hasNextPage: false,
- hasPreviousPage: false,
- totalPages: 1
+ expect(result[0]).toMatchObject({
+ item: "Item1",
+ value: 100,
+ category: { color: category1.color }
});
});
@@ -190,15 +176,72 @@ describe("expenses", () => {
});
mockGet.mockReturnValue({ value: token });
- const result = await getPaginatedExpenses();
+ const result = await getPaginatedExpenses(1, 5);
- expect(result).toEqual({
- currentPage: 1,
- expenses: [],
- hasNextPage: false,
- hasPreviousPage: false,
- totalPages: 0
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe("#getExpensesPages", () => {
+ it("should return number of expenses pages", async () => {
+ const user = await prisma.user.create({
+ data: {
+ email: "test@example.com",
+ password: { create: { hash: "hashed" } }
+ }
});
+
+ const foodCategory = await prisma.category.create({
+ data: { name: "Food", color: "#FF0000", userId: user.id }
+ });
+
+ await prisma.expense.createMany({
+ data: [
+ {
+ value: 100,
+ userId: user.id,
+ categoryId: foodCategory.id,
+ item: "Item Fruits"
+ },
+ {
+ value: 90,
+ userId: user.id,
+ categoryId: foodCategory.id,
+ item: "Item Cookies"
+ },
+ { value: 70, userId: user.id, categoryId: foodCategory.id, item: "Vegetables" },
+ { value: 50, userId: user.id, categoryId: foodCategory.id, item: "Meat" }
+ ]
+ });
+
+ const token = await encrypt({
+ userId: user.id,
+ expiresAt: new Date(Date.now() + 1000000)
+ });
+ mockGet.mockReturnValue({ value: token });
+
+ const result = await getExpensesPages("ite", 2);
+
+ expect(result).toEqual(1);
+ });
+
+ it("should return zero when user have no expenses", async () => {
+ const user = await prisma.user.create({
+ data: {
+ email: "test@example.com",
+ password: { create: { hash: "hashed" } }
+ }
+ });
+
+ const token = await encrypt({
+ userId: user.id,
+ expiresAt: new Date(Date.now() + 1000000)
+ });
+ mockGet.mockReturnValue({ value: token });
+
+ const result = await getExpensesPages("", 5);
+
+ expect(result).toEqual(0);
});
});