diff --git a/README.md b/README.md
index 12d029a..08b53c3 100644
--- a/README.md
+++ b/README.md
@@ -122,4 +122,7 @@ We are using two scopes:
1. OAuth ID, which is used to assign ownership to the application specific data when it's is stored and retrieved from our database.
2. user's e-mail address, which is stored in our database so that we can contact our users in case such action is needed.
-For any questions regarding the use of the Google API service please feel free to reach out at support@rezije.app
\ No newline at end of file
+For any questions regarding the use of the Google API service please feel free to reach out at support@rezije.app
+
+# Localization
+Localization was done by following video: https://www.youtube.com/watch?v=uZQ5d2bRMO4
\ No newline at end of file
diff --git a/app/bill/[id]/add/not-found.tsx b/app/[locale]/bill/[id]/add/not-found.tsx
similarity index 100%
rename from app/bill/[id]/add/not-found.tsx
rename to app/[locale]/bill/[id]/add/not-found.tsx
diff --git a/app/bill/[id]/add/page.tsx b/app/[locale]/bill/[id]/add/page.tsx
similarity index 100%
rename from app/bill/[id]/add/page.tsx
rename to app/[locale]/bill/[id]/add/page.tsx
diff --git a/app/bill/[id]/delete/not-found.tsx b/app/[locale]/bill/[id]/delete/not-found.tsx
similarity index 100%
rename from app/bill/[id]/delete/not-found.tsx
rename to app/[locale]/bill/[id]/delete/not-found.tsx
diff --git a/app/bill/[id]/delete/page.tsx b/app/[locale]/bill/[id]/delete/page.tsx
similarity index 100%
rename from app/bill/[id]/delete/page.tsx
rename to app/[locale]/bill/[id]/delete/page.tsx
diff --git a/app/bill/[id]/edit/not-found.tsx b/app/[locale]/bill/[id]/edit/not-found.tsx
similarity index 100%
rename from app/bill/[id]/edit/not-found.tsx
rename to app/[locale]/bill/[id]/edit/not-found.tsx
diff --git a/app/bill/[id]/edit/page.tsx b/app/[locale]/bill/[id]/edit/page.tsx
similarity index 100%
rename from app/bill/[id]/edit/page.tsx
rename to app/[locale]/bill/[id]/edit/page.tsx
diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx
new file mode 100644
index 0000000..0cdb144
--- /dev/null
+++ b/app/[locale]/layout.tsx
@@ -0,0 +1,49 @@
+import '@/app/ui/global.css';
+import { inter } from '@/app/ui/fonts';
+import { Metadata } from 'next';
+
+export const metadata:Metadata = {
+ alternates: {
+ canonical: 'https://rezije.app',
+ languages: {
+ 'en': 'https://rezije.app/en/',
+ 'hr': 'https://rezije.app/hr/',
+ }
+ },
+ openGraph: {
+ title: 'Režije',
+ description: 'Preuzmite kontrolu nad svojim režijama',
+ url: 'https://rezije.app',
+ siteName: 'Režije',
+ images: [
+ {
+ url: 'https://rezije.app/opengraph-image.png', // Must be an absolute URL
+ width: 432,
+ height: 466,
+ alt: "Režije - Preuzmite kontrolu nad svojim režijama"
+ },
+ {
+ url: 'https://rezije.app/icon6.png', // Must be an absolute URL
+ width: 256,
+ height: 256,
+ alt: "Režije - Preuzmite kontrolu nad svojim režijama"
+ },
+ ],
+ locale: 'hr',
+ type: 'website',
+ },
+}
+
+export default function RootLayout({
+ children,
+ params: { locale },
+}: {
+ children: React.ReactNode;
+ params: { locale:string };
+}) {
+ return (
+
+
{children}
+
+ );
+}
diff --git a/app/[locale]/login/page.tsx b/app/[locale]/login/page.tsx
new file mode 100644
index 0000000..d5cd131
--- /dev/null
+++ b/app/[locale]/login/page.tsx
@@ -0,0 +1,82 @@
+import { FC } from 'react';
+import { Main } from '@/app/ui/Main';
+
+import { authConfig } from "@/app/lib/auth";
+import { SignInButton } from '@/app/ui/SignInButton';
+import Image from 'next/image';
+import { getTranslations } from "next-intl/server";
+
+type Provider = {
+ id: string;
+ name: string;
+ type: string;
+ style: {
+ logo: string;
+ bg: string;
+ text: string;
+ };
+};
+
+function getProviders(): Provider[] {
+ const providerKeys: (keyof Provider)[] = ["id", "name", "type", "style"];
+ return authConfig.providers.map((provider) =>
+ getKeyValuesFromObject(provider, providerKeys)
+ );
+}
+
+function getKeyValuesFromObject(obj: any, keys: (keyof T)[]): T {
+ return keys.reduce((acc, key) => {
+ if (obj[key]) {
+ acc[key] = obj[key];
+ }
+ return acc;
+ }, {} as T);
+}
+
+const Page:FC = async () => {
+
+ const providers = await getProviders();
+ const t = await getTranslations("login-page");
+
+ return (
+
+
+ {t("main-card.title-1")}
+ {t("main-card.title-2")}
+ {t("main-card.title-3")}
+
+ {t("main-card.text-1")}
+ {t("main-card.text-2")}
+
+ {
+ Object.values(providers).map((provider) => (
+
+
+
+ ))
+ }
+
+
+
+
+
+ {t("card-1.title")}
+ {t("card-1.text")}
+
+
+
+
+ {t("card-2.title")}
+ {t("card-2.text")}
+
+
+ {t("card-3.title")}
+ {t("card-3.text")}
+
+
+
+
+ );
+}
+
+export default Page;
\ No newline at end of file
diff --git a/app/page.tsx b/app/[locale]/page.tsx
similarity index 77%
rename from app/page.tsx
rename to app/[locale]/page.tsx
index 6505d33..92efeca 100644
--- a/app/page.tsx
+++ b/app/[locale]/page.tsx
@@ -1,7 +1,7 @@
import { FC, Suspense } from 'react';
-import { Main } from './ui/Main';
-import HomePage from './ui/HomePage';
-import { MonthCardSkeleton } from './ui/MonthCardSkeleton';
+import { Main } from '@/app/ui/Main';
+import HomePage from '@/app/ui/HomePage';
+import { MonthCardSkeleton } from '@/app/ui/MonthCardSkeleton';
export interface PageProps {
searchParams?: {
diff --git a/app/policy/page.tsx b/app/[locale]/policy/page.tsx
similarity index 96%
rename from app/policy/page.tsx
rename to app/[locale]/policy/page.tsx
index ac61ac6..4f01985 100644
--- a/app/policy/page.tsx
+++ b/app/[locale]/policy/page.tsx
@@ -1,6 +1,4 @@
-import { Main } from "../ui/Main";
-import { PageFooter } from "../ui/PageFooter";
-import { PageHeader } from "../ui/PageHeader";
+import { Main } from "@/app/ui/Main";
const ConsentPage = () =>
diff --git a/app/terms/page.tsx b/app/[locale]/terms/page.tsx
similarity index 99%
rename from app/terms/page.tsx
rename to app/[locale]/terms/page.tsx
index 761231e..41cde7a 100644
--- a/app/terms/page.tsx
+++ b/app/[locale]/terms/page.tsx
@@ -1,4 +1,4 @@
-import { Main } from "../ui/Main";
+import { Main } from "@/app/ui/Main";
const TermsPage = () =>
diff --git a/app/year-month/[id]/add/page.tsx b/app/[locale]/year-month/[id]/add/page.tsx
similarity index 100%
rename from app/year-month/[id]/add/page.tsx
rename to app/[locale]/year-month/[id]/add/page.tsx
diff --git a/app/i18n.ts b/app/i18n.ts
new file mode 100644
index 0000000..fc0e549
--- /dev/null
+++ b/app/i18n.ts
@@ -0,0 +1,30 @@
+import {notFound} from 'next/navigation';
+import {getRequestConfig} from 'next-intl/server';
+import { Formats, TranslationValues } from 'next-intl';
+
+// Can be imported from a shared config
+export const locales = ['en', 'hr'];
+
+export const localeNames:Record = {
+ en: 'English',
+ hr: 'Hrvatski'
+};
+
+export const defaultLocale = 'en';
+
+/** Templating function type as returned by `useTemplate` and `getTranslations` */
+export type IntlTemplateFn =
+ // this function type if returned by `useTransations`
+ ((key: TargetKey, values?: TranslationValues | undefined, formats?: Partial | undefined) => string) |
+ // this functon type if returned by `getTranslations`
+ ((key: [TargetKey] extends [never] ? string : TargetKey, values?: TranslationValues | undefined, formats?: Partial | undefined) => string);
+
+
+export default getRequestConfig(async ({locale}) => {
+ // Validate that the incoming `locale` parameter is valid
+ if (!locales.includes(locale as any)) notFound();
+
+ return {
+ messages: (await import(`../messages/${locale}.json`)).default
+ };
+});
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
deleted file mode 100644
index bc3a4c8..0000000
--- a/app/layout.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import '@/app/ui/global.css';
-import { inter } from '@/app/ui/fonts';
-
-export default function RootLayout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- return (
-
- {children}
-
- );
-}
diff --git a/app/lib/actions/billActions.ts b/app/lib/actions/billActions.ts
index 5370e4b..0db48ab 100644
--- a/app/lib/actions/billActions.ts
+++ b/app/lib/actions/billActions.ts
@@ -7,6 +7,8 @@ import { ObjectId } from 'mongodb';
import { withUser } from '@/app/lib/auth';
import { AuthenticatedUser } from '../types/next-auth';
import { gotoHome } from './navigationActions';
+import { getTranslations } from "next-intl/server";
+import { IntlTemplateFn } from '@/app/i18n';
export type State = {
errors?: {
@@ -18,9 +20,13 @@ export type State = {
message?:string | null;
}
-const FormSchema = z.object({
+/**
+ * Schema for validating bill form fields
+ * @description this is defined as factory function so that it can be used with the next-intl library
+*/
+const FormSchema = (t:IntlTemplateFn) => z.object({
_id: z.string(),
- billName: z.coerce.string().min(1, "Bill Name is required."),
+ billName: z.coerce.string().min(1, t("bill-name-required")),
billNotes: z.string(),
payedAmount: z.string().nullable().transform((val, ctx) => {
@@ -33,7 +39,7 @@ const FormSchema = z.object({
if (isNaN(parsed)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
- message: "Not a number",
+ message: t("not-a-number"),
});
// This is a special symbol you can use to
@@ -46,7 +52,7 @@ const FormSchema = z.object({
if (parsed < 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
- message: "Value must be a positive number",
+ message: t("negative-number")
});
// This is a special symbol you can use to
@@ -61,10 +67,6 @@ const FormSchema = z.object({
}),
});
- parseFloat
-
-const UpdateBill = FormSchema.omit({ _id: true });
-
/**
* converts the file to a format stored in the database
* @param billAttachment
@@ -113,18 +115,23 @@ export const updateOrAddBill = withUser(async (user:AuthenticatedUser, locationI
const { id: userId } = user;
- const validatedFields = UpdateBill.safeParse({
- billName: formData.get('billName'),
- billNotes: formData.get('billNotes'),
- payedAmount: formData.get('payedAmount'),
- });
+ const t = await getTranslations("bill-edit-form.validation");
+
+ // FormSchema
+ const validatedFields = FormSchema(t)
+ .omit({ _id: true })
+ .safeParse({
+ billName: formData.get('billName'),
+ billNotes: formData.get('billNotes'),
+ payedAmount: formData.get('payedAmount'),
+ });
// If form validation fails, return errors early. Otherwise, continue...
if(!validatedFields.success) {
console.log("updateBill.validation-error");
return({
errors: validatedFields.error.flatten().fieldErrors,
- message: "Missing Fields. Field to Update Bill.",
+ message: t("form-error-message"),
});
}
diff --git a/app/lib/actions/locationActions.ts b/app/lib/actions/locationActions.ts
index d90d792..635acfb 100644
--- a/app/lib/actions/locationActions.ts
+++ b/app/lib/actions/locationActions.ts
@@ -8,7 +8,8 @@ import { withUser } from '@/app/lib/auth';
import { AuthenticatedUser } from '../types/next-auth';
import { gotoHome } from './navigationActions';
import { unstable_noStore as noStore } from 'next/cache';
-import { asyncTimeout } from '../asyncTimeout';
+import { IntlTemplateFn } from '@/app/i18n';
+import { getTranslations } from "next-intl/server";
export type State = {
errors?: {
@@ -18,13 +19,17 @@ export type State = {
message?:string | null;
};
-const FormSchema = z.object({
+/**
+ * Schema for validating location form fields
+ * @description this is defined as factory function so that it can be used with the next-intl library
+*/
+const FormSchema = (t:IntlTemplateFn) => z.object({
_id: z.string(),
- locationName: z.coerce.string().min(1, "Location Name is required."),
+ locationName: z.coerce.string().min(1, t("location-name-required")),
locationNotes: z.string(),
- });
-
-const UpdateLocation = FormSchema.omit({ _id: true });
+ })
+ // dont include the _id field in the response
+ .omit({ _id: true });
/**
* Server-side action which adds or updates a bill
@@ -37,7 +42,9 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
noStore();
- const validatedFields = UpdateLocation.safeParse({
+ const t = await getTranslations("location-edit-form.validation");
+
+ const validatedFields = FormSchema(t).safeParse({
locationName: formData.get('locationName'),
locationNotes: formData.get('locationNotes'),
});
@@ -84,8 +91,6 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
});
}
- // await asyncTimeout(1000);
-
if(yearMonth) await gotoHome(yearMonth);
return {
@@ -124,8 +129,6 @@ export const fetchAllLocations = withUser(async (user:AuthenticatedUser, year:nu
})
.toArray();
- // await asyncTimeout(1000);
-
return(locations)
})
@@ -154,8 +157,6 @@ export const fetchLocationById = withUser(async (user:AuthenticatedUser, locatio
return(null);
}
- // await asyncTimeout(1000);
-
return(billLocation);
})
@@ -170,7 +171,5 @@ export const deleteLocationById = withUser(async (user:AuthenticatedUser, locati
// find a location with the given locationID
const post = await dbClient.collection("lokacije").deleteOne({ _id: locationID, userId });
- // await asyncTimeout(1000);
-
await gotoHome(yearMonth)
})
\ No newline at end of file
diff --git a/app/lib/auth.ts b/app/lib/auth.ts
index 5c3eb46..f7f94b2 100644
--- a/app/lib/auth.ts
+++ b/app/lib/auth.ts
@@ -2,6 +2,7 @@ import NextAuth, { NextAuthConfig } from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import { Session } from 'next-auth';
import { AuthenticatedUser } from './types/next-auth';
+import { defaultLocale } from '../i18n';
export const authConfig: NextAuthConfig = {
callbacks: {
@@ -46,7 +47,7 @@ export const authConfig: NextAuthConfig = {
strategy: 'jwt'
},
pages: {
- signIn: '/login',
+ signIn: `/${defaultLocale}/login`,
},
};
diff --git a/app/location/[id]/add/LocationAddPage.tsx b/app/location/[id]/add/LocationAddPage.tsx
deleted file mode 100644
index 9556980..0000000
--- a/app/location/[id]/add/LocationAddPage.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { LocationEditForm } from '@/app/ui/LocationEditForm';
-import { YearMonth } from '@/app/lib/db-types';
-
-export default async function LocationAddPage({ yearMonth }: { yearMonth:YearMonth }) {
- return ( );
-}
\ No newline at end of file
diff --git a/app/location/[id]/add/page.tsx b/app/location/[id]/add/page.tsx
deleted file mode 100644
index 3f032fc..0000000
--- a/app/location/[id]/add/page.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { parseYearMonth } from '@/app/lib/format';
-import LocationAddPage from './LocationAddPage';
-import { Main } from '@/app/ui/Main';
-
-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
deleted file mode 100644
index 7377733..0000000
--- a/app/location/[id]/delete/LocationDeletePage.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { notFound } from 'next/navigation';
-import { fetchLocationById } from '@/app/lib/actions/locationActions';
-import { LocationDeleteForm } from '@/app/ui/LocationDeleteForm';
-
-export const LocationDeletePage = async ({ locationId }: { locationId:string }) => {
-
- const location = await fetchLocationById(locationId);
-
- if (!location) {
- return(notFound());
- }
-
- return ( );
-}
\ No newline at end of file
diff --git a/app/location/[id]/delete/not-found.tsx b/app/location/[id]/delete/not-found.tsx
deleted file mode 100644
index 1587224..0000000
--- a/app/location/[id]/delete/not-found.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { NotFoundPage } from '@/app/ui/NotFoundPage';
-
-const BillingLocationNotFound = () =>
- ;
-
-export default BillingLocationNotFound;
\ No newline at end of file
diff --git a/app/location/[id]/delete/page.tsx b/app/location/[id]/delete/page.tsx
deleted file mode 100644
index 40c7b8e..0000000
--- a/app/location/[id]/delete/page.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { notFound } from 'next/navigation';
-import { fetchLocationById } from '@/app/lib/actions/locationActions';
-import { LocationDeleteForm } from '@/app/ui/LocationDeleteForm';
-import { Main } from '@/app/ui/Main';
-
-export default async function Page({ params:{ id } }: { params: { id:string } }) {
-
- const location = await fetchLocationById(id);
-
- if (!location) {
- return(notFound());
- }
-
- return (
-
-
-
- );
-}
\ No newline at end of file
diff --git a/app/location/[id]/edit/LocationEditPage.tsx b/app/location/[id]/edit/LocationEditPage.tsx
deleted file mode 100644
index 505f634..0000000
--- a/app/location/[id]/edit/LocationEditPage.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { notFound } from 'next/navigation';
-import { LocationEditForm } from '@/app/ui/LocationEditForm';
-import { fetchLocationById } from '@/app/lib/actions/locationActions';
-
-export default async function LocationEditPage({ locationId }: { locationId:string }) {
-
- const location = await fetchLocationById(locationId);
-
- if (!location) {
- return(notFound());
- }
-
- const result = ;
-
- return (result);
-}
\ No newline at end of file
diff --git a/app/location/[id]/edit/not-found.tsx b/app/location/[id]/edit/not-found.tsx
deleted file mode 100644
index 54c9f60..0000000
--- a/app/location/[id]/edit/not-found.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { NotFoundPage } from '@/app/ui/NotFoundPage';
-
-const BillingLocationNotFound = () =>
- ;
-
-export default BillingLocationNotFound;
\ No newline at end of file
diff --git a/app/location/[id]/edit/page.tsx b/app/location/[id]/edit/page.tsx
deleted file mode 100644
index 251f944..0000000
--- a/app/location/[id]/edit/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Suspense } from 'react';
-import LocationEditPage from './LocationEditPage';
-import { Main } from '@/app/ui/Main';
-import { LocationEditFormSkeleton } from '@/app/ui/LocationEditForm';
-
-export default async function Page({ params:{ id } }: { params: { id:string } }) {
-
- return (
-
- }>
-
-
-
- );
-}
\ No newline at end of file
diff --git a/app/login/page.tsx b/app/login/page.tsx
deleted file mode 100644
index 054cadf..0000000
--- a/app/login/page.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { FC } from 'react';
-import { Main } from '@/app/ui/Main';
-
-import { authConfig } from "@/app/lib/auth";
-import { SignInButton } from '../ui/SignInButton';
-import Image from 'next/image';
-
-type Provider = {
- id: string;
- name: string;
- type: string;
- style: {
- logo: string;
- bg: string;
- text: string;
- };
-};
-
-function getProviders(): Provider[] {
- const providerKeys: (keyof Provider)[] = ["id", "name", "type", "style"];
- return authConfig.providers.map((provider) =>
- getKeyValuesFromObject(provider, providerKeys)
- );
-}
-
-function getKeyValuesFromObject(obj: any, keys: (keyof T)[]): T {
- return keys.reduce((acc, key) => {
- if (obj[key]) {
- acc[key] = obj[key];
- }
- return acc;
- }, {} as T);
-}
-
-const Page:FC = async () => {
-
- const providers = await getProviders()
-
- return (
-
-
- Which bills are due?
- Which are payed?
- How much are my expenses?
-
- These are the questions this simple and free app will help you with ...
- ... try it & use it completly free!
-
- {
- Object.values(providers).map((provider) => (
-
-
-
- ))
- }
-
-
-
-
- Easy copy of expenditures
- All your realestate and utilitys are automatically copied to the next month, so you don't neeed to do it by hand.
-
-
-
-
- Color signals status
- Each of trhe utility bills is color coded - at a glance you can see which bill was received and which one is payed.
-
-
- Extraction of 2D bar code
- If the attached dokument contains a 2D bar code, it is automatically extracted and shown on the page, so you can scan it without opening the PDF document.
-
-
-
-
- );
-}
-
-export default Page;
\ No newline at end of file
diff --git a/app/manifest.json b/app/manifest.json
new file mode 100644
index 0000000..5238d66
--- /dev/null
+++ b/app/manifest.json
@@ -0,0 +1,41 @@
+{
+ "name": "App Režije",
+ "short_name": "Režije",
+ "icons": [
+ {
+ "src": "/icon1.png",
+ "sizes": "16x16",
+ "type": "image/png"
+ },
+ {
+ "src": "/icon2.png",
+ "sizes": "32x32",
+ "type": "image/png"
+ },
+ {
+ "src": "/icon3.png",
+ "sizes": "48x48",
+ "type": "image/png"
+ },
+ {
+ "src": "/icon4.png",
+ "sizes": "64x64",
+ "type": "image/png"
+ },
+ {
+ "src": "/icon5.png",
+ "sizes": "128x128",
+ "type": "image/png"
+ },
+ {
+ "src": "/icon6.png",
+ "sizes": "256x256",
+ "type": "image/png"
+ }
+ ],
+ "start_url": "/",
+ "scope": "/",
+ "theme_color": "#15191e",
+ "background_color": "#15191e",
+ "display": "standalone"
+}
diff --git a/app/ui/AddLocationButton.tsx b/app/ui/AddLocationButton.tsx
index becd871..2f68524 100644
--- a/app/ui/AddLocationButton.tsx
+++ b/app/ui/AddLocationButton.tsx
@@ -2,20 +2,26 @@ import { PlusCircleIcon, HomeIcon } from "@heroicons/react/24/outline";
import { YearMonth } from "../lib/db-types";
import { formatYearMonth } from "../lib/format";
import Link from "next/link";
-
+import { useTranslations } from 'next-intl';
export interface AddLocationButtonProps {
/** year and month at which the new billing location should be addes */
yearMonth: YearMonth
}
-export const AddLocationButton:React.FC = ({yearMonth}) =>
-
-
-
-
-
- Add now realestate
-
-
-
;
\ No newline at end of file
+export const AddLocationButton:React.FC = ({yearMonth}) => {
+
+ const t = useTranslations("home-page.add-location-button");
+
+ return(
+
+
+
+
+
+ {t("tooltip")}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/ui/AddMonthButton.tsx b/app/ui/AddMonthButton.tsx
index 204df8c..ae9f1a5 100644
--- a/app/ui/AddMonthButton.tsx
+++ b/app/ui/AddMonthButton.tsx
@@ -3,18 +3,25 @@ import React from "react";
import { formatYearMonth } from "../lib/format";
import { YearMonth } from "../lib/db-types";
import Link from "next/link";
+import { useLocale, useTranslations } from 'next-intl';
export interface AddMonthButtonProps {
yearMonth: YearMonth;
}
-export const AddMonthButton:React.FC = ({ yearMonth }) =>
-
-
-
-
-
- Add next month
-
-
-
;
+export const AddMonthButton:React.FC = ({ yearMonth }) => {
+
+ const t = useTranslations("home-page.add-month-button");
+ const locale = useLocale();
+
+ return(
+
+
+
+
+
+ {t("tooltip")}
+
+
+
);
+}
diff --git a/app/ui/BillDeleteForm.tsx b/app/ui/BillDeleteForm.tsx
index 7317d8f..fcb39d9 100644
--- a/app/ui/BillDeleteForm.tsx
+++ b/app/ui/BillDeleteForm.tsx
@@ -1,11 +1,12 @@
"use client";
-import { FC } from "react";
+import { FC, ReactNode } from "react";
import { Bill, BillingLocation } from "../lib/db-types";
import { useFormState } from "react-dom";
import { Main } from "./Main";
import { deleteBillById } from "../lib/actions/billActions";
import Link from "next/link";
+import { useTranslations } from "next-intl";
export interface BillDeleteFormProps {
bill: Bill,
@@ -17,18 +18,24 @@ export const BillDeleteForm:FC = ({ bill, location }) => {
const { year, month } = location.yearMonth;
const handleAction = deleteBillById.bind(null, location._id, bill._id, year, month);
const [ state, dispatch ] = useFormState(handleAction, null);
-
+ const t = useTranslations("bill-delete-form");
return(
diff --git a/app/ui/BillEditForm.tsx b/app/ui/BillEditForm.tsx
index 10fd36a..d27ba87 100644
--- a/app/ui/BillEditForm.tsx
+++ b/app/ui/BillEditForm.tsx
@@ -8,6 +8,7 @@ import { updateOrAddBill } from "../lib/actions/billActions";
import Link from "next/link";
import { formatYearMonth } from "../lib/format";
import { findDecodePdf417 } from "../lib/pdf/barcodeDecoder";
+import { useTranslations } from "next-intl";
// Next.js does not encode an utf-8 file name correctly when sending a form with a file attachment
// This is a workaround for that
@@ -25,6 +26,8 @@ export interface BillEditFormProps {
export const BillEditForm:FC
= ({ location, bill }) => {
+ const t = useTranslations("bill-edit-form");
+
const { _id: billID, name, paid, attachment, notes, payedAmount: initialPayedAmount, barcodeImage: initialBarcodeImage } = bill ?? { _id:undefined, name:"", paid:false, notes:"" };
const { yearMonth:{year: billYear, month: billMonth}, _id: locationID } = location;
@@ -69,12 +72,12 @@ export const BillEditForm:FC = ({ location, bill }) => {
{
// don't show the delete button if we are adding a new bill
bill ?
-
+
: null
}
-
+
{state.errors?.billName &&
state.errors.billName.map((error: string) => (
@@ -107,13 +110,13 @@ export const BillEditForm:FC
= ({ location, bill }) => {
: null
}
-
+
{state.errors?.billNotes &&
state.errors.billNotes.map((error: string) => (
@@ -149,8 +152,8 @@ export const BillEditForm:FC = ({ location, bill }) => {
- Save
- Cancel
+ {t("save-button")}
+ {t("cancel-button")}
diff --git a/app/ui/LocationCard.tsx b/app/ui/LocationCard.tsx
index ae1d556..1f1120d 100644
--- a/app/ui/LocationCard.tsx
+++ b/app/ui/LocationCard.tsx
@@ -1,12 +1,13 @@
'client only';
import { Cog8ToothIcon, PlusCircleIcon } from "@heroicons/react/24/outline";
-import { FC } from "react";
+import { FC, ReactNode } from "react";
import { BillBadge } from "./BillBadge";
import { BillingLocation } from "../lib/db-types";
import { formatYearMonth } from "../lib/format";
import { formatCurrency } from "../lib/formatStrings";
import Link from "next/link";
+import { useTranslations } from "next-intl";
export interface LocationCardProps {
location: BillingLocation
@@ -14,13 +15,15 @@ export interface LocationCardProps {
export const LocationCard:FC
= ({location: { _id, name, yearMonth, bills }}) => {
+ const t = useTranslations("home-page.location-card");
+
// sum all the billAmounts
const monthlyExpense = bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0);
return(
-
+
{formatYearMonth(yearMonth)} {name}
@@ -28,14 +31,18 @@ export const LocationCard:FC
= ({location: { _id, name, yearM
{
bills.map(bill => )
}
-
+
{
monthlyExpense > 0 ?
- Payed total: { formatCurrency(monthlyExpense) }
+ {
+ t.rich("payed-total", {
+ amount: formatCurrency(monthlyExpense),
+ strong: (chunks:ReactNode) => `${chunks} `
+ })}
: null
}
diff --git a/app/ui/LocationDeleteForm.tsx b/app/ui/LocationDeleteForm.tsx
index 7f6651d..d3b8c6e 100644
--- a/app/ui/LocationDeleteForm.tsx
+++ b/app/ui/LocationDeleteForm.tsx
@@ -1,11 +1,12 @@
"use client";
-import { FC } from "react";
+import { FC, ReactNode } from "react";
import { BillingLocation } from "../lib/db-types";
import { deleteLocationById } from "../lib/actions/locationActions";
import { useFormState } from "react-dom";
import { gotoUrl } from "../lib/actions/navigationActions";
import Link from "next/link";
+import { useTranslations } from "next-intl";
export interface LocationDeleteFormProps {
/** location which should be deleted */
@@ -16,6 +17,8 @@ export const LocationDeleteForm:FC
= ({ location }) =>
{
const handleAction = deleteLocationById.bind(null, location._id, location.yearMonth);
const [ state, dispatch ] = useFormState(handleAction, null);
+ const t = useTranslations("location-delete-form");
+
const handleCancel = () => {
gotoUrl(`/location/${location._id}/edit/`);
@@ -26,11 +29,16 @@ export const LocationDeleteForm:FC = ({ location }) =>
diff --git a/app/ui/LocationEditForm.tsx b/app/ui/LocationEditForm.tsx
index 78a5af5..ae8e144 100644
--- a/app/ui/LocationEditForm.tsx
+++ b/app/ui/LocationEditForm.tsx
@@ -6,6 +6,7 @@ import { BillingLocation, YearMonth } from "../lib/db-types";
import { updateOrAddLocation } from "../lib/actions/locationActions";
import { useFormState } from "react-dom";
import Link from "next/link";
+import { useTranslations } from "next-intl";
export type LocationEditFormProps = {
/** location which should be edited */
@@ -24,6 +25,7 @@ export const LocationEditForm:FC = ({ location, yearMonth
const initialState = { message: null, errors: {} };
const handleAction = updateOrAddLocation.bind(null, location?._id, location?.yearMonth ?? yearMonth);
const [ state, dispatch ] = useFormState(handleAction, initialState);
+ const t = useTranslations("location-edit-form");
let { year, month } = location ? location.yearMonth : yearMonth;
@@ -33,11 +35,11 @@ export const LocationEditForm:FC = ({ location, yearMonth
diff --git a/app/ui/Main.tsx b/app/ui/Main.tsx
index 41b9637..3b9d044 100644
--- a/app/ui/Main.tsx
+++ b/app/ui/Main.tsx
@@ -1,16 +1,25 @@
import { FC } from "react";
import { PageHeader } from "./PageHeader";
import { PageFooter } from "./PageFooter";
+import { NextIntlClientProvider, useMessages } from "next-intl";
export interface MainProps {
children: React.ReactNode;
}
-export const Main:FC = ({ children }) =>
-
-
-
- {children}
-
-
-
\ No newline at end of file
+export const Main:FC = ({ children }) => {
+
+ const message = useMessages();
+
+ return(
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/ui/MonthCard.tsx b/app/ui/MonthCard.tsx
index 41e25ed..25e2583 100644
--- a/app/ui/MonthCard.tsx
+++ b/app/ui/MonthCard.tsx
@@ -4,7 +4,7 @@ import { FC, useEffect, useRef } from "react";
import { formatYearMonth } from "../lib/format";
import { YearMonth } from "../lib/db-types";
import { formatCurrency } from "../lib/formatStrings";
-
+import { useTranslations } from "next-intl";
export interface MonthCardProps {
yearMonth: YearMonth,
@@ -17,6 +17,7 @@ export interface MonthCardProps {
export const MonthCard:FC = ({ yearMonth, children, monthlyExpense, expanded, onToggle }) => {
const elRef = useRef(null);
+ const t = useTranslations("home-page.month-card");
// Setting the `month` will activate the accordion belonging to that month
// If the accordion is already active, it will collapse it
@@ -37,7 +38,7 @@ export const MonthCard:FC = ({ yearMonth, children, monthlyExpen
{
monthlyExpense>0 ?
- Total monthly expenditure: { formatCurrency(monthlyExpense) }
+ {t("payed-total-label")} { formatCurrency(monthlyExpense) }
: null
}
diff --git a/app/ui/PageFooter.tsx b/app/ui/PageFooter.tsx
index 0e0cbf4..7fedf90 100644
--- a/app/ui/PageFooter.tsx
+++ b/app/ui/PageFooter.tsx
@@ -1,26 +1,33 @@
import Image from "next/image";
import Link from "next/link";
import React from "react";
+import { useTranslations } from "next-intl";
-export const PageFooter: React.FC = () =>
-
-
-
-
-
-
Režije
+export const PageFooter: React.FC = () => {
+
+ const t = useTranslations("PageFooter");
+
+ return(
+
-
-
-
;
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/ui/PageHeader.tsx b/app/ui/PageHeader.tsx
index 45867ce..b009479 100644
--- a/app/ui/PageHeader.tsx
+++ b/app/ui/PageHeader.tsx
@@ -1,7 +1,10 @@
import Image from "next/image";
import Link from "next/link";
+import { SelectLanguage } from "./SelectLanguage";
export const PageHeader = () =>
-
- Režije
-
\ No newline at end of file
+
+ Režije
+
+
+
\ No newline at end of file
diff --git a/app/ui/SelectLanguage.tsx b/app/ui/SelectLanguage.tsx
new file mode 100644
index 0000000..623aaa6
--- /dev/null
+++ b/app/ui/SelectLanguage.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+import { useLocale } from "next-intl";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { defaultLocale, localeNames, locales } from "../i18n";
+
+export const SelectLanguage: React.FC = () => {
+ const currentPathname = usePathname();
+
+ const locale = useLocale();
+ const secondLocale = locales.find((l) => l !== locale) as string;
+ const secondLocalePathname = defaultLocale === locale ? `/${secondLocale}${currentPathname}` : currentPathname.replace(`/${locale}/`, `/${secondLocale}/`);
+
+ return ( {localeNames[secondLocale]});
+}
\ No newline at end of file
diff --git a/app/ui/SignInButton.tsx b/app/ui/SignInButton.tsx
index abfda18..c0c9940 100644
--- a/app/ui/SignInButton.tsx
+++ b/app/ui/SignInButton.tsx
@@ -1,6 +1,7 @@
"use client";
import { signIn } from "next-auth/react"
+import { useTranslations } from "next-intl";
import Image from "next/image";
const providerLogo = (provider: {id:string, name:string}) => {
@@ -14,10 +15,15 @@ const providerLogo = (provider: {id:string, name:string}) => {
}
}
-export const SignInButton:React.FC<{ provider: {id:string, name:string} }> = ({ provider }) =>
- signIn(provider.id, { callbackUrl:"https://rezije.app/" }) }>
-
- Sign in with {provider.name}
-
+export const SignInButton:React.FC<{ provider: {id:string, name:string} }> = ({ provider }) => {
+ const t = useTranslations("login-page");
+ return(
+ signIn(provider.id, { callbackUrl:"https://rezije.app/" }) }>
+
+
+ {t("sign-in-button")} {provider.name}
+
+ );
+}
\ No newline at end of file
diff --git a/docker-compose-deploy.yml b/docker-compose-deploy.yml
index 74e466b..506de07 100644
--- a/docker-compose-deploy.yml
+++ b/docker-compose-deploy.yml
@@ -9,7 +9,7 @@ networks:
services:
web-app:
- image: utility-bills-tracker:1.22.3
+ image: utility-bills-tracker:1.25.0
networks:
- traefik-network
- mongo-network
diff --git a/messages/en.json b/messages/en.json
new file mode 100644
index 0000000..08d78b5
--- /dev/null
+++ b/messages/en.json
@@ -0,0 +1,99 @@
+{
+ "Index": {
+ "title": "Welcome!"
+ },
+ "PageFooter": {
+ "app-description": "Helping you to stay on top of your utility bills",
+ "links": {
+ "home": "Home",
+ "privacy-policy": "Privacy Policy",
+ "terms-of-service": "Terms of Service"
+ }
+ },
+ "login-page": {
+ "main-card": {
+ "title-1": "Which bills are due?",
+ "title-2": "Which are payed?",
+ "title-3": "How much are my expenses?",
+ "text-1": "These are the questions this simple and free app will help you with ...",
+ "text-2": "... try it & use it completly free!",
+ "video-url": "/welcome-demo-vp9-25fps-1500bps.webm",
+ "image-url": "/hero.png",
+ "video-title": "Demo osnovnih koraka u aplikaciji"
+ },
+ "card-1": {
+ "title": "Easy copy of expenditures",
+ "text": "All your realestate and utilitys are automatically copied to the next month, so you don't neeed to do it by hand.",
+ "video-url": "/kopiranje-mjeseca-demo.webm",
+ "image-url": "/status-color-demo.png",
+ "video-title": "Demo kopiranja mjeseca"
+ },
+ "card-2": {
+ "title": "Color signals status",
+ "text": "Each of the utility bills is color coded - at a glance you can see which bill was received and which one is payed.",
+ "image-url": "/bar-code-demo.png",
+ "image-alt": "Boje označavaju status računa"
+ },
+ "card-3": {
+ "title": "Color signals status",
+ "text": "If the attached dokument contains a 2D bar code, it is automatically extracted and shown on the page, so you can scan it without opening the PDF document.",
+ "video-url": "/welcome-demo-vp9-25fps-1500bps.webm",
+ "image-url": "/bar-code-demo.png",
+ "video-title": "Demo osnovnih koraka u aplikaciji"
+ },
+ "sign-in-button": "Sign in with"
+ },
+ "home-page": {
+ "add-location-button": {
+ "tooltop": "Add a new realestate"
+ },
+ "add-month-button": {
+ "tooltop": "Add next mont"
+ },
+ "location-card": {
+ "edit-card-tooltip": "Edit realestate",
+ "add-bill-button-tooltip": "Add a new bill",
+ "payed-total": "Payed total: {amount} "
+ },
+ "month-card": {
+ "payed-total-label": "Total monthly expenditure:"
+ }
+ },
+ "bill-delete-form": {
+ "text": "Please confirm deletion of bill “{bill_name} ” at “{location_name} ”.",
+ "cancel-button": "Cancel",
+ "confirm-button": "Confirm"
+ },
+ "bill-edit-form": {
+ "bill-name-placeholder": "Bill name",
+ "paid-checkbox": "Paid",
+ "payed-amount": "Amount",
+ "barcode-disclaimer": "After scanning the code make sure the information is correct. We are not liable in case of an incorrect payment.",
+ "notes-placeholder": "Notes",
+ "save-button": "Save",
+ "cancel-button": "Cancel",
+ "delete-tooltip": "Delete bill",
+ "validation": {
+ "bill-name-required": "Bill name is required",
+ "payed-amount-required": "Payed amount is required",
+ "not-a-number": "Not a number",
+ "negative-number": "Value must be a positive number",
+ "form-error-message": "Form validation error. Please check the form and try again."
+ }
+ },
+ "location-delete-form": {
+ "text": "Please confirm deletion of realestate “{name} ””.",
+ "cancel-button": "Cancel",
+ "confirm-button": "Confirm"
+ },
+ "location-edit-form": {
+ "location-name-placeholder": "Realestate name",
+ "notes-placeholder": "Notes",
+ "save-button": "Save",
+ "cancel-button": "Cancel",
+ "delete-tooltip": "Delete realestate",
+ "validation": {
+ "location-name-required": "Relaestate name is required"
+ }
+ }
+}
\ No newline at end of file
diff --git a/messages/hr.json b/messages/hr.json
new file mode 100644
index 0000000..c8a3e1c
--- /dev/null
+++ b/messages/hr.json
@@ -0,0 +1,98 @@
+{
+ "Index": {
+ "title": "Dobrodošli"
+ },
+ "PageFooter": {
+ "app-description": "Preuzmite kontrolu nad svojim režijama!",
+ "links": {
+ "home": "Početna",
+ "privacy-policy": "Privatnost",
+ "terms-of-service": "Uvjeti korištenja"
+ }
+ },
+ "login-page": {
+ "main-card": {
+ "title-1": "Koji računi su stigli?",
+ "title-2": "Koji su plaćeni?",
+ "title-3": "Koliki su mi troškovi?",
+ "text-1": "To su pitanja na koja će vam ova jednostavna aplikacija odgovoriti ...",
+ "text-2": "... isprobajte je i koristite potpuno besplatno!",
+ "video-url": "/welcome-demo-vp9-25fps-1500bps.webm",
+ "image-url": "/hero.png",
+ "video-title": "Demo osnovnih koraka u aplikaciji"
+ },
+ "card-1": {
+ "title": "Prijenos režija u idući mjesec",
+ "text": "Sve vaše nekretnine i pripadajuće režije se automatski prenose u idući mjesec, tako da ne morate svaki mjesec ponovno unositi iste podatke.",
+ "video-url": "/kopiranje-mjeseca-demo.webm",
+ "image-url": "/status-color-demo.png",
+ "video-title": "Demo kopiranja mjeseca"
+ },
+ "card-2": {
+ "title": "Boja signalizira status",
+ "text": "Jednim pogledom možete vidjeti koji računi su plaćeni, a koji nisu. U tome vam pomaže boja koja označava status računa.",
+ "image-url": "/bar-code-demo.png",
+ "image-alt": "Boje označavaju status računa"
+ },
+ "card-3": {
+ "title": "Prikaz bar koda za plaćanje",
+ "text": "Ako priloženi dokument sadrži 2D barkod, on se automatski izvlači i prikazuje na stranici, tako da ga možete skenirati bez otvaranja PDF dokumenta.",
+ "video-url": "/bar-code-demo.webm",
+ "image-url": "/bar-code-demo.png",
+ "video-title": "Demo osnovnih koraka u aplikaciji"
+ },
+ "sign-in-button": "Prijavi se pomoću"
+ },
+ "home-page": {
+ "add-location-button": {
+ "tooltop": "Dodaj novu nekretninu"
+ },
+ "add-month-button": {
+ "tooltop": "Dodaj idući mjesec"
+ },
+ "location-card": {
+ "edit-card-tooltip": "Izmjeni nekretninu",
+ "add-bill-button-tooltip": "Dodaj novi račun",
+ "payed-total": "Ukupno plaćeno: {amount} "
+ },
+ "month-card": {
+ "payed-total-label": "Ukupni mjesečni trošak:"
+ }
+ },
+ "bill-delete-form": {
+ "text": "Molim potvrdi brisanje računa “{bill_name} ” koji pripada nekretnini “{location_name} ”.",
+ "cancel-button": "Odustani",
+ "confirm-button": "Potvrdi"
+ },
+ "bill-edit-form": {
+ "bill-name-placeholder": "Ime računa",
+ "paid-checkbox": "Plaćeno",
+ "payed-amount": "Iznos",
+ "barcode-disclaimer": "Nakon skeniranja bar koda obavezni provjeri jesu li svi podaci ispravni. Ne snosimo odgovornost za slučaj pogrešno provedene uplate.",
+ "notes-placeholder": "Bilješke",
+ "save-button": "Spremi",
+ "cancel-button": "Odbaci",
+ "delete-tooltip": "Obriši račun",
+ "validation": {
+ "bill-name-required": "Ime računa je obavezno",
+ "not-a-number": "Vrijednost mora biti brojka",
+ "negative-number": "Vrijednost mora biti veća od nule",
+ "form-error-message": "Forma nije ispravno popunjena. Molimo provjeri, pa pokušaj ponovno"
+ }
+ },
+ "location-delete-form": {
+ "text": "Molim potvrdi brisanje nekretnine “{name} ””.",
+ "cancel-button": "Potvrdi",
+ "confirm-button": "Odustani"
+ },
+ "location-edit-form": {
+ "location-name-placeholder": "Ime nekretnine",
+ "notes-placeholder": "Bilješke",
+ "save-button": "Spremi",
+ "cancel-button": "Odbaci",
+ "delete-tooltip": "Brisanje nekretnine",
+ "validation": {
+ "location-name-required": "Ime nekretnine je obavezno"
+ }
+ }
+}
\ No newline at end of file
diff --git a/middleware.ts b/middleware.ts
index fa7dc5d..867b6fb 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -3,11 +3,47 @@
* @description hooks-up `next-auth` into the page processing pipeline
*/
-import { auth } from '@/app/lib/auth'
-
-export default auth; // middleware will call NextAuth's `auth` method, which will in turn call) see `auth.ts`
+import { auth, authConfig } from '@/app/lib/auth'
+import createIntlMiddleware from 'next-intl/middleware';
+import { NextRequest, NextResponse } from 'next/server';
+import { locales, defaultLocale } from '@/app/i18n';
+
+const publicPages = ['/terms', '/policy', '/login'];
+const intlMiddleware = createIntlMiddleware({
+ locales,
+ localePrefix: 'as-needed',
+ defaultLocale
+});
+
+export default async function middleware(req: NextRequest) {
+ const publicPathnameRegex = RegExp(
+ `^(/(${locales.join('|')}))?(${publicPages
+ .flatMap((p) => (p === '/' ? ['', '/'] : p))
+ .join('|')})/?$`,
+ 'i'
+ );
+ const isPublicPage = publicPathnameRegex.test(req.nextUrl.pathname);
+
+ // for public pages we call only localisation middleware
+ // this is not an official way to do it - it's a hack
+ // based on https://github.com/nextauthjs/next-auth/discussions/8961
+ // The official way of chaining middlewares in AuthJS v5 does not work and is not fully documented
+ if (!isPublicPage) {
+ const session = await auth();
+
+ if (!session) {
+ const signInUrl = `${req.nextUrl.protocol}//${req.nextUrl.hostname}${req.nextUrl.port ? `:${req.nextUrl.port}` : ''}${authConfig.pages?.signIn as string}`;
+ return NextResponse.redirect( signInUrl );
+ }
+ }
+
+ return intlMiddleware(req);
+}
+
export const config = {
- // midleware will NOT be called for paths: ['/api/auth/*', '/_next/static/*', '/_next/image*']
- matcher: ['/((?!api|policy|terms|_next/static|_next/image|.*\\.png$|.*\\.webm$).*)'],
+ // for these paths middleware will not be called
+ matcher: [
+ '/((?!api|_next/static|_next/image|.*\\.png$|.*\\.webm$).*)',
+ ],
};
\ No newline at end of file
diff --git a/next.config.js b/next.config.js
index 17c5061..9393371 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,3 +1,5 @@
+const createNextIntlPlugin = require('next-intl/plugin');
+
/** @type {import('next').NextConfig} */
const nextConfig = {
// Possible options:
@@ -16,4 +18,8 @@ const nextConfig = {
}
};
-module.exports = nextConfig;
+const withNextIntl = createNextIntlPlugin('./app/i18n.ts');
+
+const nextConfigIntl = withNextIntl(nextConfig);
+
+module.exports = nextConfigIntl;
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index f3fd4c6..82075a2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "rezije",
+ "name": "evidencija-rezija",
"lockfileVersion": 3,
"requires": true,
"packages": {
@@ -17,6 +17,7 @@
"mongodb": "^6.3.0",
"next": "^14.0.2",
"next-auth": "^5.0.0-beta.4",
+ "next-intl": "^3.7.0",
"pdfjs-dist": "^4.0.379",
"pg": "^8.11.3",
"postcss": "8.4.31",
@@ -687,6 +688,92 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@formatjs/ecma402-abstract": {
+ "version": "1.18.2",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz",
+ "integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.5.4",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
+ "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@formatjs/fast-memoize": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz",
+ "integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz",
+ "integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "1.11.4",
+ "@formatjs/icu-skeleton-parser": "1.3.6",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/ecma402-abstract": {
+ "version": "1.11.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
+ "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.2.25",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/intl-localematcher": {
+ "version": "0.2.25",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
+ "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz",
+ "integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "1.11.4",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/ecma402-abstract": {
+ "version": "1.11.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
+ "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.2.25",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/intl-localematcher": {
+ "version": "0.2.25",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
+ "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/intl-localematcher": {
+ "version": "0.2.32",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz",
+ "integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@heroicons/react": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.18.tgz",
@@ -4402,6 +4489,34 @@
"node": ">= 0.4"
}
},
+ "node_modules/intl-messageformat": {
+ "version": "9.13.0",
+ "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz",
+ "integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "1.11.4",
+ "@formatjs/fast-memoize": "1.2.1",
+ "@formatjs/icu-messageformat-parser": "2.1.0",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/intl-messageformat/node_modules/@formatjs/ecma402-abstract": {
+ "version": "1.11.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
+ "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.2.25",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/intl-messageformat/node_modules/@formatjs/intl-localematcher": {
+ "version": "0.2.25",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
+ "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
@@ -5348,6 +5463,14 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/next": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/next/-/next-14.0.2.tgz",
@@ -5411,6 +5534,26 @@
}
}
},
+ "node_modules/next-intl": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.7.0.tgz",
+ "integrity": "sha512-wLewkBzUbr/g2hKkI8/M1qYzHEVT4KgDeeayppvu+aDCJSOhfUFuYg0IlGn8+HNlgos2IPRRwZtFrTusiqW+uA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/amannn"
+ }
+ ],
+ "dependencies": {
+ "@formatjs/intl-localematcher": "^0.2.32",
+ "negotiator": "^0.6.3",
+ "use-intl": "^3.7.0"
+ },
+ "peerDependencies": {
+ "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
@@ -7664,6 +7807,18 @@
"react": ">=16.8.0"
}
},
+ "node_modules/use-intl": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.7.0.tgz",
+ "integrity": "sha512-WxbrBDMWgRvoLMlvlE0LWrPmGKRydinWeatpu7QWT/ennqp0pP6Z3YwrUdPthjguByOKi/Vr5FViXhq/Fd5ifg==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "^1.11.4",
+ "intl-messageformat": "^9.3.18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index b832557..1c89582 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"mongodb": "^6.3.0",
"next": "^14.0.2",
"next-auth": "^5.0.0-beta.4",
+ "next-intl": "^3.7.0",
"pdfjs-dist": "^4.0.379",
"pg": "^8.11.3",
"postcss": "8.4.31",
diff --git a/public/opengraph-image.png b/public/opengraph-image.png
new file mode 100644
index 0000000..e0c3d13
Binary files /dev/null and b/public/opengraph-image.png differ