diff --git a/app/api/locations/available-years/route.ts b/app/api/locations/available-years/route.ts
new file mode 100644
index 0000000..310dfed
--- /dev/null
+++ b/app/api/locations/available-years/route.ts
@@ -0,0 +1,9 @@
+import { fetchAvailableYears } from '@/app/lib/actions/monthActions';
+import { NextResponse } from 'next/server';
+
+export async function GET(request: Request) {
+
+ const availableYears = await fetchAvailableYears();
+
+ return NextResponse.json({ availableYears });
+}
\ No newline at end of file
diff --git a/app/api/locations/by-id/route.ts b/app/api/locations/by-id/route.ts
new file mode 100644
index 0000000..605b5d6
--- /dev/null
+++ b/app/api/locations/by-id/route.ts
@@ -0,0 +1,13 @@
+import { fetchLocationById } from '@/app/lib/actions/locationActions';
+import type { NextApiRequest } from 'next'
+import { NextResponse } from 'next/server';
+
+export const GET = async (
+ req: NextApiRequest,
+) => {
+ const url = new URL(req.url as string);
+ const locationId = url.searchParams.get('id');
+ const location = await fetchLocationById(locationId as string);
+
+ return NextResponse.json({ location });
+}
diff --git a/app/api/locations/in-year/route.ts b/app/api/locations/in-year/route.ts
new file mode 100644
index 0000000..21179d9
--- /dev/null
+++ b/app/api/locations/in-year/route.ts
@@ -0,0 +1,14 @@
+import { fetchAllLocations } from '@/app/lib/actions/locationActions';
+import type { NextApiRequest } from 'next'
+import { NextResponse } from 'next/server';
+
+export const GET = async (
+ req: NextApiRequest,
+) => {
+ // get year from query params
+ const url = new URL(req.url as string);
+ const year = parseInt(url.searchParams.get('year') as string, 10);
+ const locations = await fetchAllLocations(year);
+
+ return NextResponse.json({ locations });
+}
diff --git a/app/lib/actions/monthActions.ts b/app/lib/actions/monthActions.ts
index 7b3d843..03dcea8 100644
--- a/app/lib/actions/monthActions.ts
+++ b/app/lib/actions/monthActions.ts
@@ -70,7 +70,7 @@ export const fetchAvailableYears = withUser(async (user:AuthenticatedUser) => {
const dbClient = await getDbClient();
// query mnogodb for all `yearMonth` values
- const years = await dbClient.collection("lokacije")
+ const years:number[] = await dbClient.collection("lokacije")
.distinct("yearMonth.year", { userId })
// sort the years in descending order
diff --git a/app/location/[id]/add/LocationAddPage.tsx b/app/location/[id]/add/LocationAddPage.tsx
index ebef49f..a099506 100644
--- a/app/location/[id]/add/LocationAddPage.tsx
+++ b/app/location/[id]/add/LocationAddPage.tsx
@@ -1,8 +1,7 @@
-import { notFound } from 'next/navigation';
+"use client";
+
import { LocationEditForm } from '@/app/ui/LocationEditForm';
-import { fetchLocationById } from '@/app/lib/actions/locationActions';
import { YearMonth } from '@/app/lib/db-types';
-import { Main } from '@/app/ui/Main';
export default async function LocationAddPage({ yearMonth }: { yearMonth:YearMonth }) {
return ();
diff --git a/app/location/[id]/add/page.tsx b/app/location/[id]/add/page.tsx
index fed90e0..87d0ddf 100644
--- a/app/location/[id]/add/page.tsx
+++ b/app/location/[id]/add/page.tsx
@@ -1,15 +1,16 @@
import { parseYearMonth } from '@/app/lib/format';
-import { LocationEditFormSkeleton } from '@/app/ui/LocationEditForm';
-import LocationAddPage from './LocationAddPage';
import { Main } from '@/app/ui/Main';
-import { Suspense } from 'react';
+import dynamic from 'next/dynamic'
+
+const LocationAddPage = dynamic(
+ () => import('./LocationAddPage'),
+ { ssr: false }
+ )
export default async function Page({ params:{ id } }: { params: { id:string } }) {
return (
- }>
-
-
+
);
}
\ No newline at end of file
diff --git a/app/location/[id]/delete/LocationDeletePage.tsx b/app/location/[id]/delete/LocationDeletePage.tsx
index 7377733..ec8663c 100644
--- a/app/location/[id]/delete/LocationDeletePage.tsx
+++ b/app/location/[id]/delete/LocationDeletePage.tsx
@@ -1,14 +1,54 @@
+"use client";
+
import { notFound } from 'next/navigation';
-import { fetchLocationById } from '@/app/lib/actions/locationActions';
-import { LocationDeleteForm } from '@/app/ui/LocationDeleteForm';
+import { LocationDeleteForm, LocationDeleteFormSkeleton } from '@/app/ui/LocationDeleteForm';
+import { WithId } from 'mongodb';
+import { BillingLocation } from '@/app/lib/db-types';
+import { useEffect, useState } from 'react';
-export const LocationDeletePage = async ({ locationId }: { locationId:string }) => {
+const fetchLocationById = async (locationId: string) => {
+ const response = await fetch(`/api/locations/by-id?id=${locationId}`);
+ const json = await response.json();
+ return json.location as WithId;
+}
- const location = await fetchLocationById(locationId);
+const LocationDeletePage = ({ locationId }: { locationId:string }) => {
+
+ const [state, stateSet] = useState<{
+ status: 'loading' | 'error' | 'success';
+ location?: WithId;
+ error?: string;
+ }>({ status: 'loading' });
- if (!location) {
- return(notFound());
+ useEffect(() => {
+
+ const fetchLocation = async () => {
+ try {
+ const location = await fetchLocationById(locationId);
+ stateSet({ location, status: 'success' });
+ } catch(error:any) {
+ stateSet({ status: 'error', error: error.message });
+ }
+ };
+
+ fetchLocation();
+
+ }, [locationId]);
+
+ switch(state.status) {
+ case "error":
+ return(Error: {state.error}
);
+ case "loading":
+ return();
+ case "success":
+ if (!state.location) {
+ return(notFound());
+ }
+
+ return();
+ default:
+ return(Error: Unknown status
);
}
+}
- return ();
-}
\ No newline at end of file
+export default LocationDeletePage;
\ No newline at end of file
diff --git a/app/location/[id]/delete/page.tsx b/app/location/[id]/delete/page.tsx
index 6ae17d5..ca0db96 100644
--- a/app/location/[id]/delete/page.tsx
+++ b/app/location/[id]/delete/page.tsx
@@ -1,17 +1,17 @@
-import { notFound } from 'next/navigation';
-import { fetchLocationById } from '@/app/lib/actions/locationActions';
-import { LocationDeleteForm } from '@/app/ui/LocationDeleteForm';
import { Main } from '@/app/ui/Main';
-import { Suspense } from 'react';
-import { LocationDeletePage } from './LocationDeletePage';
+import dynamic from 'next/dynamic'
+
+const LocationDeletePage = dynamic(
+ () => import('./LocationDeletePage'),
+ { ssr: false }
+ )
+
export default async function Page({ params:{ id } }: { params: { id:string } }) {
return (
- Loading...}>
-
-
+
);
}
\ No newline at end of file
diff --git a/app/location/[id]/edit/LocationEditPage.tsx b/app/location/[id]/edit/LocationEditPage.tsx
index 505f634..df97fad 100644
--- a/app/location/[id]/edit/LocationEditPage.tsx
+++ b/app/location/[id]/edit/LocationEditPage.tsx
@@ -1,16 +1,53 @@
+"use client";
+
import { notFound } from 'next/navigation';
-import { LocationEditForm } from '@/app/ui/LocationEditForm';
-import { fetchLocationById } from '@/app/lib/actions/locationActions';
+import { LocationEditForm, LocationEditFormSkeleton } from '@/app/ui/LocationEditForm';
+import { useEffect, useState } from 'react';
+import { WithId } from 'mongodb';
+import { BillingLocation } from '@/app/lib/db-types';
-export default async function LocationEditPage({ locationId }: { locationId:string }) {
- const location = await fetchLocationById(locationId);
+const fetchLocationById = async (locationId: string) => {
+ const response = await fetch(`/api/locations/by-id?id=${locationId}`);
+ const json = await response.json();
+ return json.location as WithId;
+}
- if (!location) {
- return(notFound());
+export default function LocationEditPage({ locationId }: { locationId:string }) {
+
+ const [state, stateSet] = useState<{
+ status: 'loading' | 'error' | 'success';
+ location?: WithId;
+ error?: string;
+ }>({ status: 'loading' });
+
+ useEffect(() => {
+
+ const fetchLocation = async () => {
+ try {
+ const location = await fetchLocationById(locationId);
+ stateSet({ location, status: 'success' });
+ } catch(error:any) {
+ stateSet({ status: 'error', error: error.message });
+ }
+ };
+
+ fetchLocation();
+
+ }, [locationId]);
+
+ switch(state.status) {
+ case "error":
+ return(Error: {state.error}
);
+ case "loading":
+ return();
+ case "success":
+ if (!state.location) {
+ return(notFound());
+ }
+
+ return();
+ default:
+ return(Error: Unknown status
);
}
-
- const result = ;
-
- return (result);
}
\ No newline at end of file
diff --git a/app/location/[id]/edit/page.tsx b/app/location/[id]/edit/page.tsx
index 251f944..0fb2160 100644
--- a/app/location/[id]/edit/page.tsx
+++ b/app/location/[id]/edit/page.tsx
@@ -1,15 +1,17 @@
-import { Suspense } from 'react';
-import LocationEditPage from './LocationEditPage';
import { Main } from '@/app/ui/Main';
-import { LocationEditFormSkeleton } from '@/app/ui/LocationEditForm';
+import dynamic from 'next/dynamic'
+
+const LocationEditPage = dynamic(
+ () => import('./LocationEditPage'),
+ { ssr: false }
+ )
+
export default async function Page({ params:{ id } }: { params: { id:string } }) {
return (
- }>
-
-
+
);
}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index d321cd7..01601df 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,7 +1,11 @@
import { FC, Suspense } from 'react';
import { Main } from './ui/Main';
-import HomePage from './ui/HomePage';
-import { HomePageSkeleton } from './ui/MonthCardSceleton';
+import dynamic from 'next/dynamic'
+
+const HomePage = dynamic(
+ () => import('./ui/HomePage'),
+ { ssr: false }
+)
export interface PageProps {
searchParams?: {
@@ -14,9 +18,7 @@ const Page:FC = async ({ searchParams }) => {
return (
- }>
-
-
+
);
}
diff --git a/app/ui/BillEditForm.tsx b/app/ui/BillEditForm.tsx
index 500ca12..b08e84c 100644
--- a/app/ui/BillEditForm.tsx
+++ b/app/ui/BillEditForm.tsx
@@ -6,7 +6,6 @@ import React, { FC } from "react";
import { useFormState } from "react-dom";
import { updateOrAddBill } from "../lib/actions/billActions";
import Link from "next/link";
-import { gotoHome } from "../lib/actions/navigationActions";
import { formatYearMonth } from "../lib/format";
// Next.js does not encode an utf-8 file name correctly when sending a form with a file attachment
@@ -35,12 +34,7 @@ export const BillEditForm:FC = ({ location, bill }) => {
const [ isPaid, setIsPaid ] = React.useState(paid);
- // redirect to the main page
- const handleCancel = () => {
- gotoHome(location.yearMonth);
- };
-
- const billPaid_handleChange = (event: React.ChangeEvent) => {
+ const billPaid_handleChange = (event: React.ChangeEvent) => {
setIsPaid(event.target.checked);
}
diff --git a/app/ui/HomePage.tsx b/app/ui/HomePage.tsx
index 2f3f59a..262da6c 100644
--- a/app/ui/HomePage.tsx
+++ b/app/ui/HomePage.tsx
@@ -1,30 +1,118 @@
-import { fetchAllLocations } from '@/app/lib/actions/locationActions';
-import { fetchAvailableYears } from '@/app/lib/actions/monthActions';
+"use client";
+
import { BillingLocation, YearMonth } from '@/app/lib/db-types';
-import { FC } from 'react';
+import { FC, useEffect, useState } from 'react';
import { MonthLocationList } from '@/app/ui/MonthLocationList';
+import { WithId } from 'mongodb';
+import { MonthCardSkeleton } from './MonthCardSkeleton';
+import { useSearchParams } from 'next/navigation';
export interface HomePageProps {
- searchParams?: {
- year?: string;
- month?: string;
- };
}
-export const HomePage:FC = async ({ searchParams }) => {
+type MonthsLocations = {
+ [key:string]:{
+ yearMonth: YearMonth,
+ locations: BillingLocation[],
+ monthlyExpense: number
+ }
+}
- let availableYears: number[];
+const fetchAllLocations = async (year: number) => {
+ const response = await fetch(`/api/locations/in-year/?year=${year}`);
+ const { locations } : { locations: WithId[] } = await response.json();
+ return locations;
+}
- // const asyncTimout = (ms:number) => new Promise(resolve => setTimeout(resolve, ms));
- // await asyncTimout(5000);
+const fetchAvailableYears = async () => {
+ const response = await fetch(`/api/locations/available-years/`);
+ const { availableYears }: { availableYears: number[]} = await response.json();
+ return availableYears;
+}
- try {
- availableYears = await fetchAvailableYears();
- } catch (error:any) {
+export const HomePage:FC = () => {
+
+ const searchParams = useSearchParams();
+ const year = searchParams.get('year');
+ const currentYear = year ? parseInt(year, 10) : new Date().getFullYear();
+
+ const [ homePageStatus, setHomePageStatus ] = useState<{
+ status: "loading" | "loaded" | "error",
+ availableYears: number[],
+ months?: MonthsLocations,
+ error?: string
+ }>({
+ status: "loading",
+ availableYears: [],
+ });
+
+ const {availableYears, months, status, error} = homePageStatus;
+
+ useEffect(() => {
+
+ const fetchData = async () => {
+
+ try {
+ const locations = await fetchAllLocations(currentYear);
+
+ // group locations by month
+ const months = locations.reduce((acc, location) => {
+ const {year, month} = location.yearMonth;
+ const key = `${year}-${month}`;
+
+ const locationsInMonth = acc[key];
+
+ if(locationsInMonth) {
+ return({
+ ...acc,
+ [key]: {
+ yearMonth: location.yearMonth,
+ locations: [...locationsInMonth.locations, location],
+ monthlyExpense: locationsInMonth.monthlyExpense + location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0)
+ }
+ })
+ }
+
+ return({
+ ...acc,
+ [key]: {
+ yearMonth: location.yearMonth,
+ locations: [location],
+ monthlyExpense: location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0)
+ }
+ });
+ }, {} as MonthsLocations);
+
+ setHomePageStatus({
+ availableYears: await fetchAvailableYears(),
+ months,
+ status: "loaded",
+ });
+
+ } catch (error: any) {
+ setHomePageStatus({
+ status: "error",
+ availableYears: [],
+ error: error.message
+ });
+ }
+ }
+
+ fetchData();
+ }, [currentYear]);
+
+ if(status === "loading") {
return (
-
- {error.message}
- );
+ <>
+
+
+
+ >
+ );
+ }
+
+ if(status === "error") {
+ return({error}
);
}
// if the database is in it's initial state, show the add location button for the current month
@@ -32,42 +120,6 @@ export const HomePage:FC = async ({ searchParams }) => {
return ();
}
- const currentYear = Number(searchParams?.year) || availableYears[0];
-
- const locations = await fetchAllLocations(currentYear);
-
- // group locations by month
- const months = locations.reduce((acc, location) => {
- const {year, month} = location.yearMonth;
- const key = `${year}-${month}`;
-
- const locationsInMonth = acc[key];
-
- if(locationsInMonth) {
- return({
- ...acc,
- [key]: {
- yearMonth: location.yearMonth,
- locations: [...locationsInMonth.locations, location],
- monthlyExpense: locationsInMonth.monthlyExpense + location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0)
- }
- })
- }
-
- return({
- ...acc,
- [key]: {
- yearMonth: location.yearMonth,
- locations: [location],
- monthlyExpense: location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0)
- }
- });
- }, {} as {[key:string]:{
- yearMonth: YearMonth,
- locations: BillingLocation[],
- monthlyExpense: number
- } });
-
return (
);
diff --git a/app/ui/LocationDeleteForm.tsx b/app/ui/LocationDeleteForm.tsx
index 35eb104..7f6651d 100644
--- a/app/ui/LocationDeleteForm.tsx
+++ b/app/ui/LocationDeleteForm.tsx
@@ -29,11 +29,23 @@ export const LocationDeleteForm:FC = ({ location }) =>
Please confirm deletion of location “{location.name}”.
-
- Cancel
+
+ Cancel
);
}
+
+
+export const LocationDeleteFormSkeleton:FC = () =>
+
\ No newline at end of file
diff --git a/app/ui/LocationEditForm.tsx b/app/ui/LocationEditForm.tsx
index 9b4aa27..b1fdc29 100644
--- a/app/ui/LocationEditForm.tsx
+++ b/app/ui/LocationEditForm.tsx
@@ -6,7 +6,6 @@ import { BillingLocation, YearMonth } from "../lib/db-types";
import { updateOrAddLocation } from "../lib/actions/locationActions";
import { useFormState } from "react-dom";
import Link from "next/link";
-import { gotoHome } from "../lib/actions/navigationActions";
export type LocationEditFormProps = {
/** location which should be edited */
@@ -48,7 +47,7 @@ export const LocationEditForm:FC = ({ location, yearMonth
))}
-
+
{state.errors?.locationNotes &&
state.errors.locationNotes.map((error: string) => (
@@ -66,10 +65,9 @@ export const LocationEditForm:FC = ({ location, yearMonth
}
-
-
- Cancel
+
+ Cancel
@@ -80,10 +78,14 @@ export const LocationEditForm:FC = ({ location, yearMonth
export const LocationEditFormSkeleton:FC = () =>
{
return(
-
+
)
diff --git a/app/ui/MonthCardSceleton.tsx b/app/ui/MonthCardSkeleton.tsx
similarity index 77%
rename from app/ui/MonthCardSceleton.tsx
rename to app/ui/MonthCardSkeleton.tsx
index bc88b4b..d074358 100644
--- a/app/ui/MonthCardSceleton.tsx
+++ b/app/ui/MonthCardSkeleton.tsx
@@ -15,18 +15,9 @@ export interface MonthCardSkeletonProps {
export const MonthCardSkeleton: React.FC
= ({checked=false}) =>
;
-
-
-export const HomePageSkeleton: React.FC = () =>
-<>
-
-
-
->;
\ No newline at end of file
diff --git a/docker-compose-deploy.yml b/docker-compose-deploy.yml
index a39518f..3dae28e 100644
--- a/docker-compose-deploy.yml
+++ b/docker-compose-deploy.yml
@@ -9,7 +9,7 @@ networks:
services:
web-app:
- image: utility-bills-tracker:1.8.0
+ image: utility-bills-tracker:1.9.0
networks:
- traefik-network
- mongo-network