From fdea05d4e336a9e37d80ac1c450c041b96b1fcef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 10:45:15 +0100 Subject: [PATCH 01/22] npm i next-intl --- package-lock.json | 155 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 156 insertions(+) diff --git a/package-lock.json b/package-lock.json index f3fd4c6..e7af0ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", From 64bd026d46f95638376e82d4c3fd54d4266cec8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 15:46:48 +0100 Subject: [PATCH 02/22] configured localization --- app/i18n.ts | 14 ++++++++++++++ messages/en.json | 5 +++++ messages/hr.json | 5 +++++ middleware.ts | 26 +++++++++++++++++++++----- next.config.js | 9 ++++++++- 5 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 app/i18n.ts create mode 100644 messages/en.json create mode 100644 messages/hr.json diff --git a/app/i18n.ts b/app/i18n.ts new file mode 100644 index 0000000..58fae1f --- /dev/null +++ b/app/i18n.ts @@ -0,0 +1,14 @@ +import {notFound} from 'next/navigation'; +import {getRequestConfig} from 'next-intl/server'; + +// Can be imported from a shared config +const locales = ['en', 'hr']; + +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/messages/en.json b/messages/en.json new file mode 100644 index 0000000..874b86c --- /dev/null +++ b/messages/en.json @@ -0,0 +1,5 @@ +{ + "Index": { + "title": "Welcome!" + } + } \ No newline at end of file diff --git a/messages/hr.json b/messages/hr.json new file mode 100644 index 0000000..4fb1e97 --- /dev/null +++ b/messages/hr.json @@ -0,0 +1,5 @@ +{ + "Index": { + "title": "Dobrodošli!" + } + } \ No newline at end of file diff --git a/middleware.ts b/middleware.ts index fa7dc5d..8e5e85f 100644 --- a/middleware.ts +++ b/middleware.ts @@ -4,10 +4,26 @@ */ import { auth } from '@/app/lib/auth' +import createIntlMiddleware from 'next-intl/middleware'; + +const locales = ['en', 'de']; +const publicPages = ['/', '/login']; -export default auth; // middleware will call NextAuth's `auth` method, which will in turn call) see `auth.ts` - +const intlMiddleware = createIntlMiddleware({ + locales, + localePrefix: 'as-needed', + defaultLocale: 'hr' +}); + 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$).*)'], -}; \ No newline at end of file + // midleware will NOT be called for paths: '/api/auth/*', '/_next/static/*', '/_next/image*', static files and public pages + matcher: [ + '/((?!api|_next/static|_next/image|.*\\.png$|.*\\.webm$|(en|hr)/(!?policy|terms|login)).*)' + ], +}; + +// middleware will call NextAuth's `auth` method, which will in turn call) see `auth.ts` +export default auth((req) => { + // call the internalization middleware + return(intlMiddleware(req)); +}); diff --git a/next.config.js b/next.config.js index 17c5061..0bdc028 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,5 @@ +import createNextIntlPlugin from 'next-intl/plugin'; + /** @type {import('next').NextConfig} */ const nextConfig = { // Possible options: @@ -16,4 +18,9 @@ const nextConfig = { } }; -module.exports = nextConfig; +const withNextIntl = createNextIntlPlugin(); + +const nextConfigIntl = withNextIntl(nextConfig); +export default nextConfigIntl; + +// module.exports = nextConfigIntl; \ No newline at end of file From ba0934e8cd0ab76863b90c4d3bdea169d31c1e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 15:51:26 +0100 Subject: [PATCH 03/22] moved all the pages into [locale] --- .../bill/[id]/add/not-found.tsx | 0 app/{ => [locale]}/bill/[id]/add/page.tsx | 0 .../bill/[id]/delete/not-found.tsx | 0 app/{ => [locale]}/bill/[id]/delete/page.tsx | 0 .../bill/[id]/edit/not-found.tsx | 0 app/{ => [locale]}/bill/[id]/edit/page.tsx | 0 app/{ => [locale]}/layout.tsx | 0 app/{ => [locale]}/login/page.tsx | 0 app/{ => [locale]}/page.tsx | 0 app/{ => [locale]}/policy/page.tsx | 0 app/{ => [locale]}/terms/page.tsx | 0 app/location/[id]/add/LocationAddPage.tsx | 6 ------ app/location/[id]/add/page.tsx | 11 ----------- .../[id]/delete/LocationDeletePage.tsx | 14 -------------- app/location/[id]/delete/not-found.tsx | 6 ------ app/location/[id]/delete/page.tsx | 19 ------------------- app/location/[id]/edit/LocationEditPage.tsx | 16 ---------------- app/location/[id]/edit/not-found.tsx | 6 ------ app/location/[id]/edit/page.tsx | 15 --------------- 19 files changed, 93 deletions(-) rename app/{ => [locale]}/bill/[id]/add/not-found.tsx (100%) rename app/{ => [locale]}/bill/[id]/add/page.tsx (100%) rename app/{ => [locale]}/bill/[id]/delete/not-found.tsx (100%) rename app/{ => [locale]}/bill/[id]/delete/page.tsx (100%) rename app/{ => [locale]}/bill/[id]/edit/not-found.tsx (100%) rename app/{ => [locale]}/bill/[id]/edit/page.tsx (100%) rename app/{ => [locale]}/layout.tsx (100%) rename app/{ => [locale]}/login/page.tsx (100%) rename app/{ => [locale]}/page.tsx (100%) rename app/{ => [locale]}/policy/page.tsx (100%) rename app/{ => [locale]}/terms/page.tsx (100%) delete mode 100644 app/location/[id]/add/LocationAddPage.tsx delete mode 100644 app/location/[id]/add/page.tsx delete mode 100644 app/location/[id]/delete/LocationDeletePage.tsx delete mode 100644 app/location/[id]/delete/not-found.tsx delete mode 100644 app/location/[id]/delete/page.tsx delete mode 100644 app/location/[id]/edit/LocationEditPage.tsx delete mode 100644 app/location/[id]/edit/not-found.tsx delete mode 100644 app/location/[id]/edit/page.tsx 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/layout.tsx b/app/[locale]/layout.tsx similarity index 100% rename from app/layout.tsx rename to app/[locale]/layout.tsx diff --git a/app/login/page.tsx b/app/[locale]/login/page.tsx similarity index 100% rename from app/login/page.tsx rename to app/[locale]/login/page.tsx diff --git a/app/page.tsx b/app/[locale]/page.tsx similarity index 100% rename from app/page.tsx rename to app/[locale]/page.tsx diff --git a/app/policy/page.tsx b/app/[locale]/policy/page.tsx similarity index 100% rename from app/policy/page.tsx rename to app/[locale]/policy/page.tsx diff --git a/app/terms/page.tsx b/app/[locale]/terms/page.tsx similarity index 100% rename from app/terms/page.tsx rename to app/[locale]/terms/page.tsx 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 From d3f9dd3d2522444726f1dd594599f38d94282dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 15:52:43 +0100 Subject: [PATCH 04/22] fixed imports of all moved files --- app/[locale]/login/page.tsx | 2 +- app/[locale]/page.tsx | 6 +++--- app/[locale]/policy/page.tsx | 6 +++--- app/[locale]/terms/page.tsx | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/[locale]/login/page.tsx b/app/[locale]/login/page.tsx index 054cadf..a476278 100644 --- a/app/[locale]/login/page.tsx +++ b/app/[locale]/login/page.tsx @@ -2,7 +2,7 @@ import { FC } from 'react'; import { Main } from '@/app/ui/Main'; import { authConfig } from "@/app/lib/auth"; -import { SignInButton } from '../ui/SignInButton'; +import { SignInButton } from '@/app/ui/SignInButton'; import Image from 'next/image'; type Provider = { diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 6505d33..92efeca 100644 --- a/app/[locale]/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/[locale]/policy/page.tsx b/app/[locale]/policy/page.tsx index ac61ac6..e64e0ac 100644 --- a/app/[locale]/policy/page.tsx +++ b/app/[locale]/policy/page.tsx @@ -1,6 +1,6 @@ -import { Main } from "../ui/Main"; -import { PageFooter } from "../ui/PageFooter"; -import { PageHeader } from "../ui/PageHeader"; +import { Main } from "@/app/ui/Main"; +import { PageFooter } from "@/app/ui/PageFooter"; +import { PageHeader } from "@/app/ui/PageHeader"; const ConsentPage = () =>
diff --git a/app/[locale]/terms/page.tsx b/app/[locale]/terms/page.tsx index 761231e..41cde7a 100644 --- a/app/[locale]/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 = () =>
From 65a7249faa8b989dae1d2b40431b0b2a068f02b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 16:01:58 +0100 Subject: [PATCH 05/22] fixed middleware exception path --- middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware.ts b/middleware.ts index 8e5e85f..708141f 100644 --- a/middleware.ts +++ b/middleware.ts @@ -18,7 +18,7 @@ const intlMiddleware = createIntlMiddleware({ export const config = { // midleware will NOT be called for paths: '/api/auth/*', '/_next/static/*', '/_next/image*', static files and public pages matcher: [ - '/((?!api|_next/static|_next/image|.*\\.png$|.*\\.webm$|(en|hr)/(!?policy|terms|login)).*)' + '/((?!api|_next/static|_next/image|.*\\.png$|.*\\.webm$|en/policy|hr/policy|en/terms|hr/terms|en/login|hr/login).*)', ], }; From e8ee913d14ee80e1edb1375c40edefc9cf01921c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 16:02:14 +0100 Subject: [PATCH 06/22] fixed next js config imports --- next.config.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/next.config.js b/next.config.js index 0bdc028..9393371 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,4 @@ -import createNextIntlPlugin from 'next-intl/plugin'; +const createNextIntlPlugin = require('next-intl/plugin'); /** @type {import('next').NextConfig} */ const nextConfig = { @@ -18,9 +18,8 @@ const nextConfig = { } }; -const withNextIntl = createNextIntlPlugin(); +const withNextIntl = createNextIntlPlugin('./app/i18n.ts'); const nextConfigIntl = withNextIntl(nextConfig); -export default nextConfigIntl; -// module.exports = nextConfigIntl; \ No newline at end of file +module.exports = nextConfigIntl; \ No newline at end of file From 8992aa76bd65292d55c342471472109fab09f9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 16:59:58 +0100 Subject: [PATCH 07/22] implemented sign-in policy --- app/i18n.ts | 2 +- app/lib/auth.ts | 2 +- middleware.ts | 45 +++++++++++++++++++++++++++++++-------------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/app/i18n.ts b/app/i18n.ts index 58fae1f..a5eefda 100644 --- a/app/i18n.ts +++ b/app/i18n.ts @@ -2,7 +2,7 @@ import {notFound} from 'next/navigation'; import {getRequestConfig} from 'next-intl/server'; // Can be imported from a shared config -const locales = ['en', 'hr']; +export const locales = ['en', 'hr']; export default getRequestConfig(async ({locale}) => { // Validate that the incoming `locale` parameter is valid diff --git a/app/lib/auth.ts b/app/lib/auth.ts index 5c3eb46..3662fa7 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -46,7 +46,7 @@ export const authConfig: NextAuthConfig = { strategy: 'jwt' }, pages: { - signIn: '/login', + signIn: '/en/login', }, }; diff --git a/middleware.ts b/middleware.ts index 708141f..4fad9e8 100644 --- a/middleware.ts +++ b/middleware.ts @@ -3,11 +3,12 @@ * @description hooks-up `next-auth` into the page processing pipeline */ -import { auth } from '@/app/lib/auth' +import { auth, authConfig } from '@/app/lib/auth' import createIntlMiddleware from 'next-intl/middleware'; +import { NextRequest, NextResponse } from 'next/server'; +import { locales } from '@/app/i18n'; -const locales = ['en', 'de']; -const publicPages = ['/', '/login']; +const publicPages = ['/terms', '/policy', '/login']; const intlMiddleware = createIntlMiddleware({ locales, @@ -15,15 +16,31 @@ const intlMiddleware = createIntlMiddleware({ defaultLocale: 'hr' }); -export const config = { - // midleware will NOT be called for paths: '/api/auth/*', '/_next/static/*', '/_next/image*', static files and public pages - matcher: [ - '/((?!api|_next/static|_next/image|.*\\.png$|.*\\.webm$|en/policy|hr/policy|en/terms|hr/terms|en/login|hr/login).*)', - ], -}; +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); -// middleware will call NextAuth's `auth` method, which will in turn call) see `auth.ts` -export default auth((req) => { - // call the internalization middleware - return(intlMiddleware(req)); -}); + // for punlic pages we call only localisation middleware + 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 = { + // for these paths middleware will not be called + matcher: [ + '/((?!api|_next/static|_next/image|.*\\.png$|.*\\.webm$).*)', + ], +}; \ No newline at end of file From 3240f746d1408c952839645969da5b90f8b7fc27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 17:02:33 +0100 Subject: [PATCH 08/22] refactor: moving locale definition --- app/i18n.ts | 2 ++ app/lib/auth.ts | 3 ++- middleware.ts | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/i18n.ts b/app/i18n.ts index a5eefda..d93ff27 100644 --- a/app/i18n.ts +++ b/app/i18n.ts @@ -3,6 +3,8 @@ import {getRequestConfig} from 'next-intl/server'; // Can be imported from a shared config export const locales = ['en', 'hr']; + +export const defaultLocale = 'en'; export default getRequestConfig(async ({locale}) => { // Validate that the incoming `locale` parameter is valid diff --git a/app/lib/auth.ts b/app/lib/auth.ts index 3662fa7..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: '/en/login', + signIn: `/${defaultLocale}/login`, }, }; diff --git a/middleware.ts b/middleware.ts index 4fad9e8..b2ab9a4 100644 --- a/middleware.ts +++ b/middleware.ts @@ -6,14 +6,14 @@ import { auth, authConfig } from '@/app/lib/auth' import createIntlMiddleware from 'next-intl/middleware'; import { NextRequest, NextResponse } from 'next/server'; -import { locales } from '@/app/i18n'; +import { locales, defaultLocale } from '@/app/i18n'; const publicPages = ['/terms', '/policy', '/login']; const intlMiddleware = createIntlMiddleware({ locales, localePrefix: 'as-needed', - defaultLocale: 'hr' + defaultLocale }); export default async function middleware(req: NextRequest) { From 50692ee6fe0d8cb1778a79e219695caf91805894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 17:12:08 +0100 Subject: [PATCH 09/22] komentiranje koda --- middleware.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/middleware.ts b/middleware.ts index b2ab9a4..867b6fb 100644 --- a/middleware.ts +++ b/middleware.ts @@ -25,7 +25,10 @@ export default async function middleware(req: NextRequest) { ); const isPublicPage = publicPathnameRegex.test(req.nextUrl.pathname); - // for punlic pages we call only localisation middleware + // 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(); From a8e40dd7597de785a04d278a2c4d28ebf650488f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 17:21:38 +0100 Subject: [PATCH 10/22] attached i18n to html language --- app/[locale]/layout.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index bc3a4c8..021c24e 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -3,11 +3,13 @@ import { inter } from '@/app/ui/fonts'; export default function RootLayout({ children, + params: { locale }, }: { children: React.ReactNode; + params: { locale:string }; }) { return ( - + {children} ); From bc230255750fe7d3f0173bd656a0127f8ed9b2c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 17:40:45 +0100 Subject: [PATCH 11/22] localized page footer --- app/ui/PageFooter.tsx | 47 +++++++++++++++++++++++++------------------ messages/en.json | 34 ++++++++++++++++++++++++++++--- messages/hr.json | 34 ++++++++++++++++++++++++++++--- 3 files changed, 89 insertions(+), 26 deletions(-) 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 = () => -
-
-
-
- logo -
Režije
+export const PageFooter: React.FC = () => { + + const t = useTranslations("PageFooter"); + + return( +
+
+
+
+ logo +
Režije
+
+

{t('app-description')}

+ {t('links.home')} + {t('links.privacy-policy')} + {t('links.terms-of-service')} +
+ -

App for helping you keeping track of your utility bills.

- Home - Privacy Policy - Terms of Service -
- -
-
; + + + ); +} \ No newline at end of file diff --git a/messages/en.json b/messages/en.json index 874b86c..d2cf6a0 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,5 +1,33 @@ { - "Index": { - "title": "Welcome!" + "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": { + "main-title-1": "Which bills are due?", + "main-title-2": "Which are payed?", + "main-title-3": "How much are my expenses?", + "main-text": "These are the questions this simple and free app will help you with ... try it & use it completly free!" + }, + "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." + }, + "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." + }, + "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." } - } \ No newline at end of file + } +} \ No newline at end of file diff --git a/messages/hr.json b/messages/hr.json index 4fb1e97..1ca528b 100644 --- a/messages/hr.json +++ b/messages/hr.json @@ -1,5 +1,33 @@ { - "Index": { - "title": "Dobrodošli!" + "Index": { + "title": "Dobrodošli" + }, + "PageFooter": { + "app-description": "Pomažemo vam pratiti vaše režije i troškove", + "links": { + "home": "Početna", + "privacy-policy": "Privatnost", + "terms-of-service": "Uvjeti korištenja" + } + }, + "login-page": { + "main-card": { + "main-title-1": "Koji računi su stigli?", + "main-title-2": "Koji su plaćeni?", + "main-title-3": "Koliki su mi troškovi?", + "main-text": "To su pitanja na koja će vam ova jednostavna aplikacija odgovoriti ... isprobajte je, koristite je potpuno besplatno!" + }, + "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." + }, + "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." + }, + "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." + } } - } \ No newline at end of file +} \ No newline at end of file From 017d52f86ebc3087a99f4f959a68f7da4aa92190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 18:03:28 +0100 Subject: [PATCH 12/22] localized login screen --- app/[locale]/login/page.tsx | 41 ++++++++++++++++++++----------------- messages/en.json | 26 ++++++++++++++++------- messages/hr.json | 26 ++++++++++++++++------- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/app/[locale]/login/page.tsx b/app/[locale]/login/page.tsx index a476278..d5cd131 100644 --- a/app/[locale]/login/page.tsx +++ b/app/[locale]/login/page.tsx @@ -4,6 +4,7 @@ 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; @@ -34,17 +35,18 @@ function getKeyValuesFromObject(obj: any, keys: (keyof T)[]): T { const Page:FC = async () => { - const providers = await getProviders() + const providers = await getProviders(); + const t = await getTranslations("login-page"); return (

- Which bills are due? - Which are payed? - How much are my expenses? + {t("main-card.title-1")} + {t("main-card.title-2")} + {t("main-card.title-3")}

-

These are the questions this simple and free app will help you with ...

-

... try it & use it completly free!

+

{t("main-card.text-1")}

+

{t("main-card.text-2")}

{ Object.values(providers).map((provider) => ( @@ -54,23 +56,24 @@ const Page:FC = async () => { )) } - -

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.

-
); diff --git a/messages/en.json b/messages/en.json index d2cf6a0..c7d9df5 100644 --- a/messages/en.json +++ b/messages/en.json @@ -12,22 +12,34 @@ }, "login-page": { "main-card": { - "main-title-1": "Which bills are due?", - "main-title-2": "Which are payed?", - "main-title-3": "How much are my expenses?", - "main-text": "These are the questions this simple and free app will help you with ... try it & use it completly free!" + "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." + "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." + "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." + "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" } } } \ No newline at end of file diff --git a/messages/hr.json b/messages/hr.json index 1ca528b..7511ce8 100644 --- a/messages/hr.json +++ b/messages/hr.json @@ -12,22 +12,34 @@ }, "login-page": { "main-card": { - "main-title-1": "Koji računi su stigli?", - "main-title-2": "Koji su plaćeni?", - "main-title-3": "Koliki su mi troškovi?", - "main-text": "To su pitanja na koja će vam ova jednostavna aplikacija odgovoriti ... isprobajte je, koristite je potpuno besplatno!" + "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, koristite je 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." + "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." + "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." + "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" } } } \ No newline at end of file From 3746989f05f456fd347a009c8b32d6feb19e70e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 18:05:44 +0100 Subject: [PATCH 13/22] readme: added link to video tutorial for localization --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 From d30bd50e1a18db4df4b47cf40b4e9dfa60bc264e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 21:56:41 +0100 Subject: [PATCH 14/22] enabled i18n for all components --- app/ui/AddLocationButton.tsx | 28 ++++++++++++-------- app/ui/AddMonthButton.tsx | 26 ++++++++++++------- app/ui/BillDeleteForm.tsx | 17 ++++++++---- app/ui/BillEditForm.tsx | 19 ++++++++------ app/ui/LocationCard.tsx | 15 ++++++++--- app/ui/LocationDeleteForm.tsx | 16 +++++++++--- app/ui/LocationEditForm.tsx | 12 +++++---- app/ui/MonthCard.tsx | 5 ++-- app/ui/SignInButton.tsx | 16 ++++++++---- messages/en.json | 49 +++++++++++++++++++++++++++++++++++ package-lock.json | 2 +- 11 files changed, 150 insertions(+), 55 deletions(-) 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..f1afa8b 100644 --- a/app/ui/AddMonthButton.tsx +++ b/app/ui/AddMonthButton.tsx @@ -3,18 +3,24 @@ import React from "react"; import { formatYearMonth } from "../lib/format"; import { YearMonth } from "../lib/db-types"; import Link from "next/link"; +import { 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"); + + 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(

- Please confirm deletion of bill “{bill.name}” at “{location.name}”. + { + t.rich("text", { + bill_name:bill.name, + location_name:location.name, + strong: (chunks:ReactNode) => `${chunks}`, + }) + }

- - Cancel + + {t("cancel-button")}
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 }) => {
@@ -134,11 +137,11 @@ export const BillEditForm:FC = ({ location, bill }) => { -

After scanning the code make sure the information is correct.
We are not liable in case of an incorrect payment.

+

{t.rich('barcode-disclaimer', { br: () =>
})}

: null } - +
{state.errors?.billNotes && state.errors.billNotes.map((error: string) => ( @@ -149,8 +152,8 @@ export const BillEditForm:FC = ({ location, bill }) => {
- - Cancel + + {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 }) =>

- Please confirm deletion of location “{location.name}”. + { + t.rich("text", { + name:location.name, + strong: (chunks:ReactNode) => `${chunks}`, + }) + }

- - Cancel + + {t("cancel-button")}
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
{ location && - + } - +
{state.errors?.locationName && state.errors.locationName.map((error: string) => ( @@ -47,7 +49,7 @@ export const LocationEditForm:FC = ({ location, yearMonth ))}
- +
{state.errors?.locationNotes && state.errors.locationNotes.map((error: string) => ( @@ -66,8 +68,8 @@ export const LocationEditForm:FC = ({ location, yearMonth }
- - Cancel + + {t("cancel-button")}
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/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 }) => - +export const SignInButton:React.FC<{ provider: {id:string, name:string} }> = ({ provider }) => { + const t = useTranslations("login-page"); + return( + + ); +} \ No newline at end of file diff --git a/messages/en.json b/messages/en.json index c7d9df5..47efa3c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -40,6 +40,55 @@ "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" + }, + "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" + } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e7af0ed..82075a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "rezije", + "name": "evidencija-rezija", "lockfileVersion": 3, "requires": true, "packages": { From 1da6479c80818877347b8da8660982d04e800fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 16 Feb 2024 22:25:09 +0100 Subject: [PATCH 15/22] i18n enabled for bill form validation --- app/lib/actions/billActions.ts | 31 ++++++++++++++++++++----------- messages/en.json | 9 ++++++++- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/app/lib/actions/billActions.ts b/app/lib/actions/billActions.ts index 5370e4b..257cf07 100644 --- a/app/lib/actions/billActions.ts +++ b/app/lib/actions/billActions.ts @@ -7,6 +7,7 @@ import { ObjectId } from 'mongodb'; import { withUser } from '@/app/lib/auth'; import { AuthenticatedUser } from '../types/next-auth'; import { gotoHome } from './navigationActions'; +import { Formats, TranslationValues, useTranslations } from "next-intl"; export type State = { errors?: { @@ -18,9 +19,12 @@ export type State = { message?:string | null; } -const FormSchema = z.object({ +type IntlTemplate = (key: TargetKey, values?: TranslationValues | undefined, formats?: Partial | undefined) => string; + + +const FormSchema = (t:IntlTemplate) => 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 +37,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 +50,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 @@ -63,7 +67,7 @@ const FormSchema = z.object({ parseFloat -const UpdateBill = FormSchema.omit({ _id: true }); +const UpdateBill = ; /** * converts the file to a format stored in the database @@ -113,18 +117,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 = useTranslations("bill-edit-form.validation"); + + // FormSchema + const validatedFields = UpdateBill(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/messages/en.json b/messages/en.json index 47efa3c..0652b45 100644 --- a/messages/en.json +++ b/messages/en.json @@ -74,7 +74,14 @@ "notes-placeholder": "Notes", "save-button": "Save", "cancel-button": "Cancel", - "delete-tooltip": "Delete bill" + "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": { From 30b3da9c318f54e14e59555f672ba9ccc34eedda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Sat, 17 Feb 2024 07:28:47 +0100 Subject: [PATCH 16/22] i18n support added to all form validations --- app/i18n.ts | 11 ++++++++++- app/lib/actions/billActions.ts | 20 +++++++++----------- app/lib/actions/locationActions.ts | 29 ++++++++++++++--------------- messages/en.json | 8 +++++--- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/app/i18n.ts b/app/i18n.ts index d93ff27..a539866 100644 --- a/app/i18n.ts +++ b/app/i18n.ts @@ -1,11 +1,20 @@ 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 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(); diff --git a/app/lib/actions/billActions.ts b/app/lib/actions/billActions.ts index 257cf07..0db48ab 100644 --- a/app/lib/actions/billActions.ts +++ b/app/lib/actions/billActions.ts @@ -7,7 +7,8 @@ import { ObjectId } from 'mongodb'; import { withUser } from '@/app/lib/auth'; import { AuthenticatedUser } from '../types/next-auth'; import { gotoHome } from './navigationActions'; -import { Formats, TranslationValues, useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; +import { IntlTemplateFn } from '@/app/i18n'; export type State = { errors?: { @@ -19,10 +20,11 @@ export type State = { message?:string | null; } -type IntlTemplate = (key: TargetKey, values?: TranslationValues | undefined, formats?: Partial | undefined) => string; - - -const FormSchema = (t:IntlTemplate) => 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, t("bill-name-required")), billNotes: z.string(), @@ -65,10 +67,6 @@ const FormSchema = (t:IntlTemplate) => z.object({ }), }); - parseFloat - -const UpdateBill = ; - /** * converts the file to a format stored in the database * @param billAttachment @@ -117,10 +115,10 @@ export const updateOrAddBill = withUser(async (user:AuthenticatedUser, locationI const { id: userId } = user; - const t = useTranslations("bill-edit-form.validation"); + const t = await getTranslations("bill-edit-form.validation"); // FormSchema - const validatedFields = UpdateBill(t) + const validatedFields = FormSchema(t) .omit({ _id: true }) .safeParse({ billName: formData.get('billName'), 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/messages/en.json b/messages/en.json index 0652b45..3384c8a 100644 --- a/messages/en.json +++ b/messages/en.json @@ -95,7 +95,9 @@ "notes-placeholder": "Notes", "save-button": "Save", "cancel-button": "Cancel", - "delete-tooltip": "Delete realestate" - - } + "delete-tooltip": "Delete realestate", + "validation": { + "location-name-required": "Relaestate name is required" + } + } } \ No newline at end of file From 327bc99d35afe6adfc37b75b27cf1f1ebf37d63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Sat, 17 Feb 2024 07:39:58 +0100 Subject: [PATCH 17/22] completed HR translation --- messages/en.json | 34 ++++++------- messages/hr.json | 127 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 105 insertions(+), 56 deletions(-) diff --git a/messages/en.json b/messages/en.json index 3384c8a..08d78b5 100644 --- a/messages/en.json +++ b/messages/en.json @@ -5,11 +5,11 @@ "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" - } - }, + "home": "Home", + "privacy-policy": "Privacy Policy", + "terms-of-service": "Terms of Service" + } + }, "login-page": { "main-card": { "title-1": "Which bills are due?", @@ -22,7 +22,7 @@ "video-title": "Demo osnovnih koraka u aplikaciji" }, "card-1": { - "title":"Easy copy of expenditures", + "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", @@ -55,20 +55,18 @@ "add-bill-button-tooltip": "Add a new bill", "payed-total": "Payed total: {amount}" }, - "month-card": { + "month-card": { "payed-total-label": "Total monthly expenditure:" } }, - "bill-delete-form": - { + "bill-delete-form": { "text": "Please confirm deletion of bill “{bill_name}” at “{location_name}”.", "cancel-button": "Cancel", "confirm-button": "Confirm" }, - "bill-edit-form": - { + "bill-edit-form": { "bill-name-placeholder": "Bill name", - "paid-checkbox":"Paid", + "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", @@ -81,16 +79,14 @@ "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": - { + "location-delete-form": { "text": "Please confirm deletion of realestate “{name}””.", "cancel-button": "Cancel", "confirm-button": "Confirm" }, - "location-edit-form": - { + "location-edit-form": { "location-name-placeholder": "Realestate name", "notes-placeholder": "Notes", "save-button": "Save", @@ -98,6 +94,6 @@ "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 index 7511ce8..c8a3e1c 100644 --- a/messages/hr.json +++ b/messages/hr.json @@ -3,43 +3,96 @@ "title": "Dobrodošli" }, "PageFooter": { - "app-description": "Pomažemo vam pratiti vaše režije i troškove", + "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, koristite je 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" - } + "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 From 4f6d31a7c19bb91b49bbf0694ea62abe475b141b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Sat, 17 Feb 2024 07:44:22 +0100 Subject: [PATCH 18/22] moved page under locale --- app/{ => [locale]}/year-month/[id]/add/page.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/{ => [locale]}/year-month/[id]/add/page.tsx (100%) 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 From 1c6694028787787f042e325b37c10d0c50c6ab96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Sat, 17 Feb 2024 08:32:53 +0100 Subject: [PATCH 19/22] added language switcher --- app/[locale]/policy/page.tsx | 2 -- app/i18n.ts | 5 +++++ app/ui/AddMonthButton.tsx | 5 +++-- app/ui/Main.tsx | 25 +++++++++++++++++-------- app/ui/PageHeader.tsx | 9 ++++++--- app/ui/SelectLanguage.tsx | 16 ++++++++++++++++ 6 files changed, 47 insertions(+), 15 deletions(-) create mode 100644 app/ui/SelectLanguage.tsx diff --git a/app/[locale]/policy/page.tsx b/app/[locale]/policy/page.tsx index e64e0ac..4f01985 100644 --- a/app/[locale]/policy/page.tsx +++ b/app/[locale]/policy/page.tsx @@ -1,6 +1,4 @@ import { Main } from "@/app/ui/Main"; -import { PageFooter } from "@/app/ui/PageFooter"; -import { PageHeader } from "@/app/ui/PageHeader"; const ConsentPage = () =>
diff --git a/app/i18n.ts b/app/i18n.ts index a539866..fc0e549 100644 --- a/app/i18n.ts +++ b/app/i18n.ts @@ -5,6 +5,11 @@ 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` */ diff --git a/app/ui/AddMonthButton.tsx b/app/ui/AddMonthButton.tsx index f1afa8b..ae9f1a5 100644 --- a/app/ui/AddMonthButton.tsx +++ b/app/ui/AddMonthButton.tsx @@ -3,7 +3,7 @@ import React from "react"; import { formatYearMonth } from "../lib/format"; import { YearMonth } from "../lib/db-types"; import Link from "next/link"; -import { useTranslations } from 'next-intl'; +import { useLocale, useTranslations } from 'next-intl'; export interface AddMonthButtonProps { yearMonth: YearMonth; @@ -12,10 +12,11 @@ export interface AddMonthButtonProps { export const AddMonthButton:React.FC = ({ yearMonth }) => { const t = useTranslations("home-page.add-month-button"); + const locale = useLocale(); return(
- + 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/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 = () => -
- logo Režije -
\ No newline at end of file +
+ logo 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 From 8145f149aa311186e23b979a15f1d5f816272cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Sat, 17 Feb 2024 08:51:36 +0100 Subject: [PATCH 20/22] added opengraph metadata --- app/[locale]/layout.tsx | 33 +++++++++++++++++++++++++++++++++ public/opengraph-image.png | Bin 0 -> 35782 bytes 2 files changed, 33 insertions(+) create mode 100644 public/opengraph-image.png diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 021c24e..0cdb144 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -1,5 +1,38 @@ 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, diff --git a/public/opengraph-image.png b/public/opengraph-image.png new file mode 100644 index 0000000000000000000000000000000000000000..e0c3d1365e06848376035be65c9bd110dcb4ad0e GIT binary patch literal 35782 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

8LQ02=z|D`rt#2NTK_IVyDBXi7 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�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 GLK6US!g%Qb literal 0 HcmV?d00001 From 74fbfef125342c59812153e8c1a064cbf71d15dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Sat, 17 Feb 2024 09:01:01 +0100 Subject: [PATCH 21/22] implemented web manifest --- app/manifest.json | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 app/manifest.json 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" +} From 1a3220840b7d35427b4ebabdb618699c47900583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Sat, 17 Feb 2024 09:01:50 +0100 Subject: [PATCH 22/22] updated version in compose file --- docker-compose-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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