From e6815840d62ffe07429d24415bfb8a2760ddb459 Mon Sep 17 00:00:00 2001 From: TroyMoses Date: Mon, 24 Feb 2025 18:38:31 +0300 Subject: [PATCH 01/26] installed next-intl package, added some config files, changed file structure --- website2/next.config.mjs | 6 +- website2/package-lock.json | 284 +++++++- website2/package.json | 1 + website2/public/locales/en.json | 25 + website2/public/locales/fr.json | 24 + .../{ => [locale]}/(about)/about-us/page.tsx | 42 +- .../(about)/careers/[id]/page.tsx | 22 +- .../{ => [locale]}/(about)/careers/layout.tsx | 34 +- .../{ => [locale]}/(about)/careers/page.tsx | 22 +- .../(about)/events/[id]/page.tsx | 22 +- .../{ => [locale]}/(about)/events/layout.tsx | 34 +- .../{ => [locale]}/(about)/events/page.tsx | 26 +- .../src/app/{ => [locale]}/(about)/layout.tsx | 26 +- .../app/{ => [locale]}/(about)/press/page.tsx | 42 +- .../{ => [locale]}/(about)/resources/page.tsx | 42 +- .../app/{ => [locale]}/MaintenancePage.tsx | 108 +-- .../clean-air-forum/about/page.tsx | 26 +- .../clean-air-forum/glossary/page.tsx | 230 +++--- .../{ => [locale]}/clean-air-forum/layout.tsx | 152 ++-- .../clean-air-forum/logistics/page.tsx | 192 +++--- .../clean-air-forum/partners/page.tsx | 296 ++++---- .../program-committee/page.tsx | 242 +++---- .../clean-air-forum/resources/page.tsx | 296 ++++---- .../clean-air-forum/sessions/page.tsx | 316 ++++----- .../clean-air-forum/speakers/page.tsx | 294 ++++---- .../clean-air-forum/sponsorships/page.tsx | 164 ++--- .../clean-air-network/CleanAirPage.tsx | 392 +++++------ .../clean-air-network/events/[id]/page.tsx | 26 +- .../clean-air-network/events/page.tsx | 22 +- .../clean-air-network/layout.tsx | 84 +-- .../clean-air-network/membership/page.tsx | 22 +- .../{ => [locale]}/clean-air-network/page.tsx | 26 +- .../clean-air-network/resources/page.tsx | 22 +- .../{ => [locale]}/contact/ContactPage.tsx | 232 +++---- .../{ => [locale]}/contact/form/FormPage.tsx | 536 +++++++------- .../app/{ => [locale]}/contact/form/page.tsx | 26 +- .../src/app/{ => [locale]}/contact/layout.tsx | 58 +- .../src/app/{ => [locale]}/contact/page.tsx | 26 +- .../contact/success/SuccessPage.tsx | 240 +++---- .../{ => [locale]}/contact/success/page.tsx | 26 +- website2/src/app/{ => [locale]}/error.tsx | 144 ++-- .../explore-data/ExplorePage.tsx | 324 ++++----- .../{ => [locale]}/explore-data/layout.tsx | 34 +- .../explore-data/mobile-app/page.tsx | 230 +++--- .../app/{ => [locale]}/explore-data/page.tsx | 26 +- website2/src/app/{ => [locale]}/home/page.tsx | 28 +- website2/src/app/{ => [locale]}/layout.tsx | 27 +- .../{ => [locale]}/legal/airqo-data/page.tsx | 26 +- .../src/app/{ => [locale]}/legal/layout.tsx | 62 +- .../legal/payment-refund-policy/page.tsx | 26 +- .../legal/privacy-policy/page.tsx | 26 +- .../legal/terms-of-service/page.tsx | 26 +- website2/src/app/{ => [locale]}/not-found.tsx | 116 ++-- website2/src/app/{ => [locale]}/page.tsx | 0 .../app/{ => [locale]}/partners/[id]/page.tsx | 262 +++---- .../app/{ => [locale]}/partners/layout.tsx | 64 +- .../products/analytics/AnalyticsPage.tsx | 588 ++++++++-------- .../products/analytics/page.tsx | 26 +- .../{ => [locale]}/products/api/ApiPage.tsx | 530 +++++++------- .../app/{ => [locale]}/products/api/page.tsx | 26 +- .../products/calibrate/CalibratePage.tsx | 554 +++++++-------- .../products/calibrate/page.tsx | 26 +- .../app/{ => [locale]}/products/layout.tsx | 44 +- .../products/mobile-app/MobilePage.tsx | 592 ++++++++-------- .../products/mobile-app/page.tsx | 26 +- .../products/monitor/MonitorPage.tsx | 652 +++++++++--------- .../{ => [locale]}/products/monitor/page.tsx | 26 +- .../solutions/african-cities/page.tsx | 22 +- .../solutions/communities/page.tsx | 22 +- .../app/{ => [locale]}/solutions/layout.tsx | 48 +- .../solutions/research/page.tsx | 22 +- website2/src/components/layouts/Navbar.tsx | 3 +- website2/src/config.ts | 12 + website2/src/i18n.ts | 15 + website2/src/middleware.ts | 12 + website2/src/navigation.ts | 6 + website2/src/views/home/HomePage.tsx | 38 +- 77 files changed, 4897 insertions(+), 4520 deletions(-) create mode 100644 website2/public/locales/en.json create mode 100644 website2/public/locales/fr.json rename website2/src/app/{ => [locale]}/(about)/about-us/page.tsx (96%) rename website2/src/app/{ => [locale]}/(about)/careers/[id]/page.tsx (94%) rename website2/src/app/{ => [locale]}/(about)/careers/layout.tsx (96%) rename website2/src/app/{ => [locale]}/(about)/careers/page.tsx (93%) rename website2/src/app/{ => [locale]}/(about)/events/[id]/page.tsx (94%) rename website2/src/app/{ => [locale]}/(about)/events/layout.tsx (96%) rename website2/src/app/{ => [locale]}/(about)/events/page.tsx (93%) rename website2/src/app/{ => [locale]}/(about)/layout.tsx (95%) rename website2/src/app/{ => [locale]}/(about)/press/page.tsx (96%) rename website2/src/app/{ => [locale]}/(about)/resources/page.tsx (96%) rename website2/src/app/{ => [locale]}/MaintenancePage.tsx (97%) rename website2/src/app/{ => [locale]}/clean-air-forum/about/page.tsx (93%) rename website2/src/app/{ => [locale]}/clean-air-forum/glossary/page.tsx (97%) rename website2/src/app/{ => [locale]}/clean-air-forum/layout.tsx (96%) rename website2/src/app/{ => [locale]}/clean-air-forum/logistics/page.tsx (97%) rename website2/src/app/{ => [locale]}/clean-air-forum/partners/page.tsx (97%) rename website2/src/app/{ => [locale]}/clean-air-forum/program-committee/page.tsx (97%) rename website2/src/app/{ => [locale]}/clean-air-forum/resources/page.tsx (96%) rename website2/src/app/{ => [locale]}/clean-air-forum/sessions/page.tsx (96%) rename website2/src/app/{ => [locale]}/clean-air-forum/speakers/page.tsx (97%) rename website2/src/app/{ => [locale]}/clean-air-forum/sponsorships/page.tsx (96%) rename website2/src/app/{ => [locale]}/clean-air-network/CleanAirPage.tsx (97%) rename website2/src/app/{ => [locale]}/clean-air-network/events/[id]/page.tsx (94%) rename website2/src/app/{ => [locale]}/clean-air-network/events/page.tsx (93%) rename website2/src/app/{ => [locale]}/clean-air-network/layout.tsx (96%) rename website2/src/app/{ => [locale]}/clean-air-network/membership/page.tsx (94%) rename website2/src/app/{ => [locale]}/clean-air-network/page.tsx (93%) rename website2/src/app/{ => [locale]}/clean-air-network/resources/page.tsx (94%) rename website2/src/app/{ => [locale]}/contact/ContactPage.tsx (96%) rename website2/src/app/{ => [locale]}/contact/form/FormPage.tsx (96%) rename website2/src/app/{ => [locale]}/contact/form/page.tsx (92%) rename website2/src/app/{ => [locale]}/contact/layout.tsx (97%) rename website2/src/app/{ => [locale]}/contact/page.tsx (93%) rename website2/src/app/{ => [locale]}/contact/success/SuccessPage.tsx (96%) rename website2/src/app/{ => [locale]}/contact/success/page.tsx (93%) rename website2/src/app/{ => [locale]}/error.tsx (96%) rename website2/src/app/{ => [locale]}/explore-data/ExplorePage.tsx (97%) rename website2/src/app/{ => [locale]}/explore-data/layout.tsx (97%) rename website2/src/app/{ => [locale]}/explore-data/mobile-app/page.tsx (97%) rename website2/src/app/{ => [locale]}/explore-data/page.tsx (93%) rename website2/src/app/{ => [locale]}/home/page.tsx (94%) rename website2/src/app/{ => [locale]}/layout.tsx (64%) rename website2/src/app/{ => [locale]}/legal/airqo-data/page.tsx (93%) rename website2/src/app/{ => [locale]}/legal/layout.tsx (96%) rename website2/src/app/{ => [locale]}/legal/payment-refund-policy/page.tsx (93%) rename website2/src/app/{ => [locale]}/legal/privacy-policy/page.tsx (93%) rename website2/src/app/{ => [locale]}/legal/terms-of-service/page.tsx (93%) rename website2/src/app/{ => [locale]}/not-found.tsx (96%) rename website2/src/app/{ => [locale]}/page.tsx (100%) rename website2/src/app/{ => [locale]}/partners/[id]/page.tsx (96%) rename website2/src/app/{ => [locale]}/partners/layout.tsx (97%) rename website2/src/app/{ => [locale]}/products/analytics/AnalyticsPage.tsx (97%) rename website2/src/app/{ => [locale]}/products/analytics/page.tsx (93%) rename website2/src/app/{ => [locale]}/products/api/ApiPage.tsx (97%) rename website2/src/app/{ => [locale]}/products/api/page.tsx (92%) rename website2/src/app/{ => [locale]}/products/calibrate/CalibratePage.tsx (97%) rename website2/src/app/{ => [locale]}/products/calibrate/page.tsx (93%) rename website2/src/app/{ => [locale]}/products/layout.tsx (97%) rename website2/src/app/{ => [locale]}/products/mobile-app/MobilePage.tsx (97%) rename website2/src/app/{ => [locale]}/products/mobile-app/page.tsx (92%) rename website2/src/app/{ => [locale]}/products/monitor/MonitorPage.tsx (97%) rename website2/src/app/{ => [locale]}/products/monitor/page.tsx (93%) rename website2/src/app/{ => [locale]}/solutions/african-cities/page.tsx (94%) rename website2/src/app/{ => [locale]}/solutions/communities/page.tsx (94%) rename website2/src/app/{ => [locale]}/solutions/layout.tsx (97%) rename website2/src/app/{ => [locale]}/solutions/research/page.tsx (94%) create mode 100644 website2/src/config.ts create mode 100644 website2/src/i18n.ts create mode 100644 website2/src/middleware.ts create mode 100644 website2/src/navigation.ts diff --git a/website2/next.config.mjs b/website2/next.config.mjs index 60418cc7cc..c23d815bfc 100644 --- a/website2/next.config.mjs +++ b/website2/next.config.mjs @@ -1,3 +1,7 @@ +import createNextIntlPlugin from 'next-intl/plugin'; + +const withNextIntl = createNextIntlPlugin(); + /** @type {import('next').NextConfig} */ const nextConfig = { images: { @@ -50,4 +54,4 @@ const nextConfig = { }, }; -export default nextConfig; +export default withNextIntl(nextConfig); diff --git a/website2/package-lock.json b/website2/package-lock.json index 1286c59924..ad08921739 100644 --- a/website2/package-lock.json +++ b/website2/package-lock.json @@ -27,6 +27,7 @@ "glob": "^9.3.5", "lucide-react": "^0.447.0", "next": "^14.2.23", + "next-intl": "^3.26.4", "quill-delta-to-html": "^0.12.1", "react": "^18", "react-dom": "^18", @@ -515,7 +516,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -706,6 +706,66 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.3.tgz", + "integrity": "sha512-pJT1OkhplSmvvr6i3CWTPvC/FGC06MbN5TNBfRO6Ox62AEz90eMq+dVvtX9Bl3jxCEkS0tATzDarRZuOLw7oFg==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.6", + "@formatjs/intl-localematcher": "0.6.0", + "decimal.js": "10", + "tslib": "2" + } + }, + "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.0.tgz", + "integrity": "sha512-4rB4g+3hESy1bHSBG3tDFaMY2CH67iT7yne1e+0CLTsGLDcmoEWWpJjjpWVaYgYfYuohIRuo0E+N536gd2ZHZA==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.6.tgz", + "integrity": "sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.1.tgz", + "integrity": "sha512-o0AhSNaOfKoic0Sn1GkFCK4MxdRsw7mPJ5/rBpIqdvcC7MIuyUSW8WChUEvrK78HhNpYOgqCQbINxCTumJLzZA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.3", + "@formatjs/icu-skeleton-parser": "1.8.13", + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.13", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.13.tgz", + "integrity": "sha512-N/LIdTvVc1TpJmMt2jVg0Fr1F7Q1qJPdZSCs19unMskCmVQ/sa0H9L8PWt13vq+gLdLg1+pPsvBLydL1Apahjg==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.3", + "tslib": "2" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", + "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -3121,6 +3181,16 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", + "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -3205,14 +3275,12 @@ "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "devOptional": true + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==" }, "node_modules/@types/react": { "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", - "devOptional": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4293,6 +4361,17 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/core-js": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz", + "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -4347,8 +4426,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -4432,6 +4510,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "license": "MIT" + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -6002,12 +6086,36 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -6017,6 +6125,43 @@ "node": ">=10.17.0" } }, + "node_modules/i18next": { + "version": "24.2.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.2.tgz", + "integrity": "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-fs-backend": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.6.0.tgz", + "integrity": "sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw==", + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6124,6 +6269,18 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "10.7.15", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.15.tgz", + "integrity": "sha512-LRyExsEsefQSBjU2p47oAheoKz+EOJxSLDdjOaEjdriajfHsMXOmV/EhMvYSg9bAgCUHasuAC+mcUBe/95PfIg==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.3", + "@formatjs/fast-memoize": "2.2.6", + "@formatjs/icu-messageformat-parser": "2.11.1", + "tslib": "2" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -7959,6 +8116,15 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next": { "version": "14.2.23", "resolved": "https://registry.npmjs.org/next/-/next-14.2.23.tgz", @@ -8008,6 +8174,63 @@ } } }, + "node_modules/next-i18next": { + "version": "15.4.2", + "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-15.4.2.tgz", + "integrity": "sha512-zgRxWf7kdXtM686ecGIBQL+Bq0+DqAhRlasRZ3vVF0TmrNTWkVhs52n//oU3Fj5O7r/xOKkECDUwfOuXVwTK/g==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@types/hoist-non-react-statics": "^3.3.6", + "core-js": "^3", + "hoist-non-react-statics": "^3.3.2", + "i18next-fs-backend": "^2.6.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "i18next": ">= 23.7.13", + "next": ">= 12.0.0", + "react": ">= 17.0.2", + "react-i18next": ">= 13.5.0" + } + }, + "node_modules/next-intl": { + "version": "3.26.4", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.26.4.tgz", + "integrity": "sha512-/kFFR7WYJGisOR0xKoC930e6oTOOWf8rbHviQgte5zIn6OgJ6mKFvXI94RWAW3ksCZJCvE4zblIuYwHCSbbw7g==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.5.4", + "negotiator": "^1.0.0", + "use-intl": "^3.26.4" + }, + "peerDependencies": { + "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -8793,6 +9016,28 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" }, + "node_modules/react-i18next": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.1.tgz", + "integrity": "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-icons": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", @@ -8982,8 +9227,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.3", @@ -10179,7 +10423,7 @@ "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10268,6 +10512,19 @@ } } }, + "node_modules/use-intl": { + "version": "3.26.4", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.26.4.tgz", + "integrity": "sha512-5DhN+YfsocNO7LiLpns7/pxRcMHA4DgBZQo5Z6uw3LvX9XIZyPAdRBdFPE2eBKTAwhY77k5eBhxqDtx8wzUaBg==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^2.2.0", + "intl-messageformat": "^10.5.14" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/use-sidecar": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", @@ -10316,6 +10573,15 @@ "node": ">=10.12.0" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", diff --git a/website2/package.json b/website2/package.json index 4fff66f730..31362d0b63 100644 --- a/website2/package.json +++ b/website2/package.json @@ -33,6 +33,7 @@ "glob": "^9.3.5", "lucide-react": "^0.447.0", "next": "^14.2.23", + "next-intl": "^3.26.4", "quill-delta-to-html": "^0.12.1", "react": "^18", "react-dom": "^18", diff --git a/website2/public/locales/en.json b/website2/public/locales/en.json new file mode 100644 index 0000000000..996e5d43fe --- /dev/null +++ b/website2/public/locales/en.json @@ -0,0 +1,25 @@ +{ + "home": { + "title": "Home | AirQo", + "description": "Explore the air quality monitoring data and tools by AirQo.", + "keywords": "air quality, pollution, monitoring, AirQo, environment", + "highResolutionTitle": "High-resolution air quality monitoring network", + "airQualityMonitor": "Air Quality Monitor", + "monitorDescription": "We deploy a high-resolution air quality monitoring network in target urban areas across Africa to increase awareness and understanding of air quality management, provide actionable information, and derive actions against air pollution.", + "learnMore": "Learn more", + "analyticsTitle": "An interactive air quality analytics platform", + "airQualityAnalytics": "Air Quality Analytics", + "analyticsDescription": "Access and visualise real-time and historical air quality information across Africa through our easy-to-use air quality analytics dashboard.", + "apiTitle": "Amplify air quality impact through our API", + "airQualityAPI": "Air Quality API", + "apiDescription": "Are you a developer? We invite you to leverage our open-air quality data on your App", + "getStartedHere": "Get started here", + "mapTitle": "Live air quality insights across Africa", + "airQualityMap": "Air Quality Map", + "mapDescription": "Visualize hourly air quality information with a single click, over our growing network across African cities", + "viewMore": "View more", + "downloadApp": "Download the app", + "appDescription": "Discover the quality of air you are breathing" + } + } + \ No newline at end of file diff --git a/website2/public/locales/fr.json b/website2/public/locales/fr.json new file mode 100644 index 0000000000..f5d7922da4 --- /dev/null +++ b/website2/public/locales/fr.json @@ -0,0 +1,24 @@ +{ + "home": { + "title": "Accueil | AirQo", + "description": "Explorez les données et les outils de surveillance de la qualité de l'air par AirQo.", + "keywords": "qualité de l'air, pollution, surveillance, AirQo, environnement", + "highResolutionTitle": "Réseau de surveillance de la qualité de l'air à haute résolution", + "airQualityMonitor": "Moniteur de qualité de l'air", + "monitorDescription": "Nous déployons un réseau de surveillance de la qualité de l'air à haute résolution dans les zones urbaines cibles à travers l'Afrique pour accroître la sensibilisation et la compréhension de la gestion de la qualité de l'air, fournir des informations exploitables et dériver des actions contre la pollution de l'air.", + "learnMore": "En savoir plus", + "analyticsTitle": "Une plateforme interactive d'analyse de la qualité de l'air", + "airQualityAnalytics": "Analyses de la qualité de l'air", + "analyticsDescription": "Accédez et visualisez des informations en temps réel et historiques sur la qualité de l'air à travers l'Afrique grâce à notre tableau de bord d'analyse de la qualité de l'air facile à utiliser.", + "apiTitle": "Amplifiez l'impact sur la qualité de l'air grâce à notre API", + "airQualityAPI": "API de qualité de l'air", + "apiDescription": "Vous êtes développeur ? Nous vous invitons à exploiter nos données ouvertes sur la qualité de l'air dans votre application", + "getStartedHere": "Commencez ici", + "mapTitle": "Informations en direct sur la qualité de l'air en Afrique", + "airQualityMap": "Carte de la qualité de l'air", + "mapDescription": "Visualisez les informations horaires sur la qualité de l'air en un seul clic, sur notre réseau croissant dans les villes africaines", + "viewMore": "Voir plus", + "downloadApp": "Téléchargez l'application", + "appDescription": "Découvrez la qualité de l'air que vous respirez" + } + } \ No newline at end of file diff --git a/website2/src/app/(about)/about-us/page.tsx b/website2/src/app/[locale]/(about)/about-us/page.tsx similarity index 96% rename from website2/src/app/(about)/about-us/page.tsx rename to website2/src/app/[locale]/(about)/about-us/page.tsx index 55da3203aa..864b1e397e 100644 --- a/website2/src/app/(about)/about-us/page.tsx +++ b/website2/src/app/[locale]/(about)/about-us/page.tsx @@ -1,21 +1,21 @@ -import { Metadata } from 'next'; - -import AboutPage from '@/views/about/AboutPage'; - -export const metadata: Metadata = { - title: 'About Us | AirQo', - description: - 'Discover AirQo’s mission to monitor and improve air quality in Africa. Learn more about our work, partnerships, and impact.', - keywords: - 'AirQo, about AirQo, air quality, Africa, pollution monitoring, environmental monitoring', -}; - -const page = () => { - return ( -
- -
- ); -}; - -export default page; +import { Metadata } from 'next'; + +import AboutPage from '@/views/about/AboutPage'; + +export const metadata: Metadata = { + title: 'About Us | AirQo', + description: + 'Discover AirQo’s mission to monitor and improve air quality in Africa. Learn more about our work, partnerships, and impact.', + keywords: + 'AirQo, about AirQo, air quality, Africa, pollution monitoring, environmental monitoring', +}; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/website2/src/app/(about)/careers/[id]/page.tsx b/website2/src/app/[locale]/(about)/careers/[id]/page.tsx similarity index 94% rename from website2/src/app/(about)/careers/[id]/page.tsx rename to website2/src/app/[locale]/(about)/careers/[id]/page.tsx index 26a5bc1bb9..4f794b5337 100644 --- a/website2/src/app/(about)/careers/[id]/page.tsx +++ b/website2/src/app/[locale]/(about)/careers/[id]/page.tsx @@ -1,11 +1,11 @@ -import DetailsPage from '@/views/careers/DetailsPage'; - -const page = ({ params }: { params: any }) => { - return ( -
- -
- ); -}; - -export default page; +import DetailsPage from '@/views/careers/DetailsPage'; + +const page = ({ params }: { params: any }) => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/website2/src/app/(about)/careers/layout.tsx b/website2/src/app/[locale]/(about)/careers/layout.tsx similarity index 96% rename from website2/src/app/(about)/careers/layout.tsx rename to website2/src/app/[locale]/(about)/careers/layout.tsx index 0b8ad62683..f29fa030eb 100644 --- a/website2/src/app/(about)/careers/layout.tsx +++ b/website2/src/app/[locale]/(about)/careers/layout.tsx @@ -1,17 +1,17 @@ -import { Metadata } from 'next'; - -export const metadata: Metadata = { - title: 'Careers | Join the AirQo Team', - description: - 'Explore career opportunities at AirQo and join us in improving air quality across Africa. See how you can contribute to our mission and make an impact.', - keywords: - 'AirQo careers, jobs at AirQo, air quality jobs, environmental jobs, AirQo team, work at AirQo', -}; - -export default function CareersLayout({ - children, -}: { - children: React.ReactNode; -}) { - return <>{children}; -} +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Careers | Join the AirQo Team', + description: + 'Explore career opportunities at AirQo and join us in improving air quality across Africa. See how you can contribute to our mission and make an impact.', + keywords: + 'AirQo careers, jobs at AirQo, air quality jobs, environmental jobs, AirQo team, work at AirQo', +}; + +export default function CareersLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/website2/src/app/(about)/careers/page.tsx b/website2/src/app/[locale]/(about)/careers/page.tsx similarity index 93% rename from website2/src/app/(about)/careers/page.tsx rename to website2/src/app/[locale]/(about)/careers/page.tsx index f4bc355b5e..ea37e12595 100644 --- a/website2/src/app/(about)/careers/page.tsx +++ b/website2/src/app/[locale]/(about)/careers/page.tsx @@ -1,11 +1,11 @@ -import CareerPage from '@/views/careers/CareerPage'; - -const page = () => { - return ( -
- -
- ); -}; - -export default page; +import CareerPage from '@/views/careers/CareerPage'; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/website2/src/app/(about)/events/[id]/page.tsx b/website2/src/app/[locale]/(about)/events/[id]/page.tsx similarity index 94% rename from website2/src/app/(about)/events/[id]/page.tsx rename to website2/src/app/[locale]/(about)/events/[id]/page.tsx index d57fff950e..084fd0bb17 100644 --- a/website2/src/app/(about)/events/[id]/page.tsx +++ b/website2/src/app/[locale]/(about)/events/[id]/page.tsx @@ -1,11 +1,11 @@ -import SingleEvent from '@/views/events/SingleEvent'; - -const page = ({ params }: { params: any }) => { - return ( -
- -
- ); -}; - -export default page; +import SingleEvent from '@/views/events/SingleEvent'; + +const page = ({ params }: { params: any }) => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/website2/src/app/(about)/events/layout.tsx b/website2/src/app/[locale]/(about)/events/layout.tsx similarity index 96% rename from website2/src/app/(about)/events/layout.tsx rename to website2/src/app/[locale]/(about)/events/layout.tsx index 900e14121c..9c448f59f6 100644 --- a/website2/src/app/(about)/events/layout.tsx +++ b/website2/src/app/[locale]/(about)/events/layout.tsx @@ -1,17 +1,17 @@ -import { Metadata } from 'next'; - -export const metadata: Metadata = { - title: 'Events | AirQo', - description: - 'Explore upcoming and past events hosted by AirQo to raise awareness and promote actions for air quality improvement.', - keywords: - 'AirQo events, air quality events, environmental events, AirQo conferences, air quality workshops', -}; - -export default function EventsLayout({ - children, -}: { - children: React.ReactNode; -}) { - return <>{children}; -} +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Events | AirQo', + description: + 'Explore upcoming and past events hosted by AirQo to raise awareness and promote actions for air quality improvement.', + keywords: + 'AirQo events, air quality events, environmental events, AirQo conferences, air quality workshops', +}; + +export default function EventsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/website2/src/app/(about)/events/page.tsx b/website2/src/app/[locale]/(about)/events/page.tsx similarity index 93% rename from website2/src/app/(about)/events/page.tsx rename to website2/src/app/[locale]/(about)/events/page.tsx index 5719428b82..8d6bf4af4f 100644 --- a/website2/src/app/(about)/events/page.tsx +++ b/website2/src/app/[locale]/(about)/events/page.tsx @@ -1,13 +1,13 @@ -import React from 'react'; - -import EventPage from '@/views/events/EventPage'; - -const page = () => { - return ( -
- -
- ); -}; - -export default page; +import React from 'react'; + +import EventPage from '@/views/events/EventPage'; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/website2/src/app/(about)/layout.tsx b/website2/src/app/[locale]/(about)/layout.tsx similarity index 95% rename from website2/src/app/(about)/layout.tsx rename to website2/src/app/[locale]/(about)/layout.tsx index 4011c429fb..86754caeb7 100644 --- a/website2/src/app/(about)/layout.tsx +++ b/website2/src/app/[locale]/(about)/layout.tsx @@ -1,13 +1,13 @@ -import React from 'react'; - -import MainLayout from '@/components/layouts/MainLayout'; - -type AboutLayoutProps = { - children: React.ReactNode; -}; - -const AboutLayout: React.FC = ({ children }) => { - return {children}; -}; - -export default AboutLayout; +import React from 'react'; + +import MainLayout from '@/components/layouts/MainLayout'; + +type AboutLayoutProps = { + children: React.ReactNode; +}; + +const AboutLayout: React.FC = ({ children }) => { + return {children}; +}; + +export default AboutLayout; diff --git a/website2/src/app/(about)/press/page.tsx b/website2/src/app/[locale]/(about)/press/page.tsx similarity index 96% rename from website2/src/app/(about)/press/page.tsx rename to website2/src/app/[locale]/(about)/press/page.tsx index e38496414e..6bbfbf4ade 100644 --- a/website2/src/app/(about)/press/page.tsx +++ b/website2/src/app/[locale]/(about)/press/page.tsx @@ -1,21 +1,21 @@ -import { Metadata } from 'next'; - -import PressPage from '@/views/press/PressPage'; - -export const metadata: Metadata = { - title: 'Press | AirQo in the News', - description: - 'Stay updated with the latest news and media coverage about AirQo’s efforts to monitor and improve air quality in Africa.', - keywords: - 'AirQo press, AirQo news, air quality news, AirQo media, air pollution in Africa, AirQo coverage', -}; - -const page = () => { - return ( -
- -
- ); -}; - -export default page; +import { Metadata } from 'next'; + +import PressPage from '@/views/press/PressPage'; + +export const metadata: Metadata = { + title: 'Press | AirQo in the News', + description: + 'Stay updated with the latest news and media coverage about AirQo’s efforts to monitor and improve air quality in Africa.', + keywords: + 'AirQo press, AirQo news, air quality news, AirQo media, air pollution in Africa, AirQo coverage', +}; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/website2/src/app/(about)/resources/page.tsx b/website2/src/app/[locale]/(about)/resources/page.tsx similarity index 96% rename from website2/src/app/(about)/resources/page.tsx rename to website2/src/app/[locale]/(about)/resources/page.tsx index 87640eb11f..c0e888c4e7 100644 --- a/website2/src/app/(about)/resources/page.tsx +++ b/website2/src/app/[locale]/(about)/resources/page.tsx @@ -1,21 +1,21 @@ -import { Metadata } from 'next'; - -import ResourcePage from '@/views/publications/ResourcePage'; - -export const metadata: Metadata = { - title: 'Resources | Air Quality Data and Tools by AirQo', - description: - 'Access AirQo’s air quality data, research, and tools to help monitor and improve environmental health in Africa. Explore our datasets and resources.', - keywords: - 'AirQo resources, air quality data, environmental data, air pollution, AirQo tools, air quality research, air quality reports', -}; - -const page = () => { - return ( -
- -
- ); -}; - -export default page; +import { Metadata } from 'next'; + +import ResourcePage from '@/views/publications/ResourcePage'; + +export const metadata: Metadata = { + title: 'Resources | Air Quality Data and Tools by AirQo', + description: + 'Access AirQo’s air quality data, research, and tools to help monitor and improve environmental health in Africa. Explore our datasets and resources.', + keywords: + 'AirQo resources, air quality data, environmental data, air pollution, AirQo tools, air quality research, air quality reports', +}; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/website2/src/app/MaintenancePage.tsx b/website2/src/app/[locale]/MaintenancePage.tsx similarity index 97% rename from website2/src/app/MaintenancePage.tsx rename to website2/src/app/[locale]/MaintenancePage.tsx index be4cd10a28..855dd2750c 100644 --- a/website2/src/app/MaintenancePage.tsx +++ b/website2/src/app/[locale]/MaintenancePage.tsx @@ -1,54 +1,54 @@ -'use client'; -import { motion } from 'framer-motion'; -import React from 'react'; - -interface MaintenancePageProps { - message: string; -} - -const MaintenancePage: React.FC = ({ message }) => { - return ( - -
- - Site Under Maintenance - - - {message || - 'We are currently upgrading our website to serve you better. Please check back soon.'} - - - If you need assistance, feel free to reach us at{' '} - - support@airqo.net - - -
-
- ); -}; - -export default MaintenancePage; +'use client'; +import { motion } from 'framer-motion'; +import React from 'react'; + +interface MaintenancePageProps { + message: string; +} + +const MaintenancePage: React.FC = ({ message }) => { + return ( + +
+ + Site Under Maintenance + + + {message || + 'We are currently upgrading our website to serve you better. Please check back soon.'} + + + If you need assistance, feel free to reach us at{' '} + + support@airqo.net + + +
+
+ ); +}; + +export default MaintenancePage; diff --git a/website2/src/app/clean-air-forum/about/page.tsx b/website2/src/app/[locale]/clean-air-forum/about/page.tsx similarity index 93% rename from website2/src/app/clean-air-forum/about/page.tsx rename to website2/src/app/[locale]/clean-air-forum/about/page.tsx index 20109a061c..6f34af39c6 100644 --- a/website2/src/app/clean-air-forum/about/page.tsx +++ b/website2/src/app/[locale]/clean-air-forum/about/page.tsx @@ -1,13 +1,13 @@ -import React from 'react'; - -import AboutPage from '@/views/Forum/AboutPage'; - -const page = () => { - return ( -
- -
- ); -}; - -export default page; +import React from 'react'; + +import AboutPage from '@/views/Forum/AboutPage'; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/website2/src/app/clean-air-forum/glossary/page.tsx b/website2/src/app/[locale]/clean-air-forum/glossary/page.tsx similarity index 97% rename from website2/src/app/clean-air-forum/glossary/page.tsx rename to website2/src/app/[locale]/clean-air-forum/glossary/page.tsx index d099dd930e..afa66a6464 100644 --- a/website2/src/app/clean-air-forum/glossary/page.tsx +++ b/website2/src/app/[locale]/clean-air-forum/glossary/page.tsx @@ -1,115 +1,115 @@ -'use client'; - -import DOMPurify from 'dompurify'; -import Link from 'next/link'; -import React from 'react'; - -import Loading from '@/components/loading'; -import { Divider } from '@/components/ui'; -import { NoData } from '@/components/ui'; -import { useForumData } from '@/context/ForumDataContext'; -import { ForumEvent } from '@/types/forum'; -import { isValidGlossaryContent } from '@/utils/glossaryValidator'; -import { renderContent } from '@/utils/quillUtils'; -import SectionDisplay from '@/views/Forum/SectionDisplay'; - -const GlossaryPage: React.FC = () => { - // Access data from the context. - const { selectedEvent, eventTitles } = useForumData(); - - // If either is not available, show a loading state. - if (!selectedEvent || !eventTitles) { - return ; - } - - // Extract the events list from eventTitles. - // If eventTitles is an array, use it directly; otherwise, assume it's a ForumTitlesResponse. - const eventsList: ForumEvent[] = Array.isArray(eventTitles) - ? eventTitles - : eventTitles.forum_events; - - if (eventsList.length === 0) { - return ; - } - - // Render the main glossary content using the selected event. - const glossaryHTML = renderContent(selectedEvent.glossary_details); - const showGlossaryMain = isValidGlossaryContent(glossaryHTML); - - const glossarySections = selectedEvent.sections?.filter((section: any) => { - if (!section.pages.includes('glossary')) return false; - const html = renderContent(section.content); - return html.trim().length > 0; - }); - - return ( -
- - - {/* Clean Air Forum Events Section (Sidebar) */} -
- {/* Left column: Heading */} -
-

- Clean Air Forum Events -

-
- {/* Right column: List of event links */} -
-
    - {eventsList.map((event) => { - // Use the unique_title directly in the link. - const href = `/clean-air-forum/about?slug=${encodeURIComponent( - event.unique_title, - )}`; - return ( -
  • - - {event.title} - -
  • - ); - })} -
-
-
- - {/* Clean Air Glossary Section */} - {showGlossaryMain && ( - <> - -
- {/* Left column: Heading */} -
-

- Clean Air Glossary -

-
- {/* Right column: Glossary content */} -
-
- - )} - - {/* Additional Glossary Sections (if any) */} - {glossarySections && glossarySections.length > 0 && ( - <> - {glossarySections.map((section: any) => ( - - ))} - - )} -
- ); -}; - -export default GlossaryPage; +'use client'; + +import DOMPurify from 'dompurify'; +import Link from 'next/link'; +import React from 'react'; + +import Loading from '@/components/loading'; +import { Divider } from '@/components/ui'; +import { NoData } from '@/components/ui'; +import { useForumData } from '@/context/ForumDataContext'; +import { ForumEvent } from '@/types/forum'; +import { isValidGlossaryContent } from '@/utils/glossaryValidator'; +import { renderContent } from '@/utils/quillUtils'; +import SectionDisplay from '@/views/Forum/SectionDisplay'; + +const GlossaryPage: React.FC = () => { + // Access data from the context. + const { selectedEvent, eventTitles } = useForumData(); + + // If either is not available, show a loading state. + if (!selectedEvent || !eventTitles) { + return ; + } + + // Extract the events list from eventTitles. + // If eventTitles is an array, use it directly; otherwise, assume it's a ForumTitlesResponse. + const eventsList: ForumEvent[] = Array.isArray(eventTitles) + ? eventTitles + : eventTitles.forum_events; + + if (eventsList.length === 0) { + return ; + } + + // Render the main glossary content using the selected event. + const glossaryHTML = renderContent(selectedEvent.glossary_details); + const showGlossaryMain = isValidGlossaryContent(glossaryHTML); + + const glossarySections = selectedEvent.sections?.filter((section: any) => { + if (!section.pages.includes('glossary')) return false; + const html = renderContent(section.content); + return html.trim().length > 0; + }); + + return ( +
+ + + {/* Clean Air Forum Events Section (Sidebar) */} +
+ {/* Left column: Heading */} +
+

+ Clean Air Forum Events +

+
+ {/* Right column: List of event links */} +
+
    + {eventsList.map((event) => { + // Use the unique_title directly in the link. + const href = `/clean-air-forum/about?slug=${encodeURIComponent( + event.unique_title, + )}`; + return ( +
  • + + {event.title} + +
  • + ); + })} +
+
+
+ + {/* Clean Air Glossary Section */} + {showGlossaryMain && ( + <> + +
+ {/* Left column: Heading */} +
+

+ Clean Air Glossary +

+
+ {/* Right column: Glossary content */} +
+
+ + )} + + {/* Additional Glossary Sections (if any) */} + {glossarySections && glossarySections.length > 0 && ( + <> + {glossarySections.map((section: any) => ( + + ))} + + )} +
+ ); +}; + +export default GlossaryPage; diff --git a/website2/src/app/clean-air-forum/layout.tsx b/website2/src/app/[locale]/clean-air-forum/layout.tsx similarity index 96% rename from website2/src/app/clean-air-forum/layout.tsx rename to website2/src/app/[locale]/clean-air-forum/layout.tsx index 260174194f..8bebfaf96d 100644 --- a/website2/src/app/clean-air-forum/layout.tsx +++ b/website2/src/app/[locale]/clean-air-forum/layout.tsx @@ -1,76 +1,76 @@ -// components/layouts/CleanAirLayout.tsx -'use client'; - -import { useSearchParams } from 'next/navigation'; -import React, { ReactNode } from 'react'; - -import Footer from '@/components/layouts/Footer'; -import Navbar from '@/components/layouts/Navbar'; -import NewsLetter from '@/components/layouts/NewsLetter'; -import Loading from '@/components/loading'; -import { NoData } from '@/components/ui'; -import mainConfig from '@/configs/mainConfigs'; -import { ForumDataProvider } from '@/context/ForumDataContext'; -import { useForumEventDetails, useForumEventTitles } from '@/hooks/useApiHooks'; -import BannerSection from '@/views/Forum/BannerSection'; - -type CleanAirLayoutProps = { - children: ReactNode; -}; - -const CleanAirLayout: React.FC = ({ children }) => { - const searchParams = useSearchParams(); - // Get the slug query parameter; if missing, the hook returns the "latest" event. - const slug = searchParams.get('slug'); - - // Get the details for the selected (or latest) event. - const { data: selectedEvent, isLoading: detailsLoading } = - useForumEventDetails(slug); - // Get the list of event titles (with unique_title values). - const { data: eventTitles, isLoading: titlesLoading } = useForumEventTitles(); - - if (detailsLoading || titlesLoading) { - return ; - } - - if (!selectedEvent) { - return ; - } - - // Provide both the selected event and the list of event titles to the context. - const forumData = { - selectedEvent, - eventTitles, - }; - - return ( - -
- {/* Navbar */} -
- -
- - {/* Banner Section */} - - - {/* Main Content */} -
- {children} -
- - {/* Newsletter Section */} -
- -
- - {/* Footer */} -
-
-
-
-
- ); -}; - -export default CleanAirLayout; +// components/layouts/CleanAirLayout.tsx +'use client'; + +import { useSearchParams } from 'next/navigation'; +import React, { ReactNode } from 'react'; + +import Footer from '@/components/layouts/Footer'; +import Navbar from '@/components/layouts/Navbar'; +import NewsLetter from '@/components/layouts/NewsLetter'; +import Loading from '@/components/loading'; +import { NoData } from '@/components/ui'; +import mainConfig from '@/configs/mainConfigs'; +import { ForumDataProvider } from '@/context/ForumDataContext'; +import { useForumEventDetails, useForumEventTitles } from '@/hooks/useApiHooks'; +import BannerSection from '@/views/Forum/BannerSection'; + +type CleanAirLayoutProps = { + children: ReactNode; +}; + +const CleanAirLayout: React.FC = ({ children }) => { + const searchParams = useSearchParams(); + // Get the slug query parameter; if missing, the hook returns the "latest" event. + const slug = searchParams.get('slug'); + + // Get the details for the selected (or latest) event. + const { data: selectedEvent, isLoading: detailsLoading } = + useForumEventDetails(slug); + // Get the list of event titles (with unique_title values). + const { data: eventTitles, isLoading: titlesLoading } = useForumEventTitles(); + + if (detailsLoading || titlesLoading) { + return ; + } + + if (!selectedEvent) { + return ; + } + + // Provide both the selected event and the list of event titles to the context. + const forumData = { + selectedEvent, + eventTitles, + }; + + return ( + +
+ {/* Navbar */} +
+ +
+ + {/* Banner Section */} + + + {/* Main Content */} +
+ {children} +
+ + {/* Newsletter Section */} +
+ +
+ + {/* Footer */} +
+
+
+
+
+ ); +}; + +export default CleanAirLayout; diff --git a/website2/src/app/clean-air-forum/logistics/page.tsx b/website2/src/app/[locale]/clean-air-forum/logistics/page.tsx similarity index 97% rename from website2/src/app/clean-air-forum/logistics/page.tsx rename to website2/src/app/[locale]/clean-air-forum/logistics/page.tsx index abbadd07df..d1af67519d 100644 --- a/website2/src/app/clean-air-forum/logistics/page.tsx +++ b/website2/src/app/[locale]/clean-air-forum/logistics/page.tsx @@ -1,96 +1,96 @@ -'use client'; - -import DOMPurify from 'dompurify'; -import React from 'react'; - -import Loading from '@/components/loading'; -import { Divider } from '@/components/ui'; -import { useForumData } from '@/context/ForumDataContext'; -import { isValidHTMLContent } from '@/utils/htmlValidator'; -import { renderContent } from '@/utils/quillUtils'; -import SectionDisplay from '@/views/Forum/SectionDisplay'; - -const LogisticsPage: React.FC = () => { - // Destructure the selected event from the context. - const { selectedEvent } = useForumData(); - - // If selectedEvent is not available, show a loading state. - if (!selectedEvent) { - return ; - } - - // Render static content from the event model. - const vaccinationHTML = renderContent( - selectedEvent.travel_logistics_vaccination_details, - ); - const visaHTML = renderContent(selectedEvent.travel_logistics_visa_details); - - const showVaccination = isValidHTMLContent(vaccinationHTML); - const showVisa = isValidHTMLContent(visaHTML); - - // Filter extra sections assigned to the "logistics" page. - const logisticsSections = selectedEvent.sections?.filter( - (section: any) => - section.pages.includes('logistics') && - isValidHTMLContent(renderContent(section.content)), - ); - - return ( -
- {/* Render Vaccination Section if content exists */} - {showVaccination && ( - <> - -
-
-
-

- Vaccination -

-
-
-
-
- - )} - - {/* Render Visa Invitation Letter Section if content exists */} - {showVisa && ( - <> - -
-
-
-

- Visa invitation letter -

-
-
-
-
- - )} - - {/* Render additional Logistics Sections, if any */} - {logisticsSections && logisticsSections.length > 0 && ( - <> - {logisticsSections.map((section: any) => ( - - ))} - - )} -
- ); -}; - -export default LogisticsPage; +'use client'; + +import DOMPurify from 'dompurify'; +import React from 'react'; + +import Loading from '@/components/loading'; +import { Divider } from '@/components/ui'; +import { useForumData } from '@/context/ForumDataContext'; +import { isValidHTMLContent } from '@/utils/htmlValidator'; +import { renderContent } from '@/utils/quillUtils'; +import SectionDisplay from '@/views/Forum/SectionDisplay'; + +const LogisticsPage: React.FC = () => { + // Destructure the selected event from the context. + const { selectedEvent } = useForumData(); + + // If selectedEvent is not available, show a loading state. + if (!selectedEvent) { + return ; + } + + // Render static content from the event model. + const vaccinationHTML = renderContent( + selectedEvent.travel_logistics_vaccination_details, + ); + const visaHTML = renderContent(selectedEvent.travel_logistics_visa_details); + + const showVaccination = isValidHTMLContent(vaccinationHTML); + const showVisa = isValidHTMLContent(visaHTML); + + // Filter extra sections assigned to the "logistics" page. + const logisticsSections = selectedEvent.sections?.filter( + (section: any) => + section.pages.includes('logistics') && + isValidHTMLContent(renderContent(section.content)), + ); + + return ( +
+ {/* Render Vaccination Section if content exists */} + {showVaccination && ( + <> + +
+
+
+

+ Vaccination +

+
+
+
+
+ + )} + + {/* Render Visa Invitation Letter Section if content exists */} + {showVisa && ( + <> + +
+
+
+

+ Visa invitation letter +

+
+
+
+
+ + )} + + {/* Render additional Logistics Sections, if any */} + {logisticsSections && logisticsSections.length > 0 && ( + <> + {logisticsSections.map((section: any) => ( + + ))} + + )} +
+ ); +}; + +export default LogisticsPage; diff --git a/website2/src/app/clean-air-forum/partners/page.tsx b/website2/src/app/[locale]/clean-air-forum/partners/page.tsx similarity index 97% rename from website2/src/app/clean-air-forum/partners/page.tsx rename to website2/src/app/[locale]/clean-air-forum/partners/page.tsx index ab8ecb1556..a09501359d 100644 --- a/website2/src/app/clean-air-forum/partners/page.tsx +++ b/website2/src/app/[locale]/clean-air-forum/partners/page.tsx @@ -1,148 +1,148 @@ -'use client'; - -import DOMPurify from 'dompurify'; -import React from 'react'; - -import { Divider } from '@/components/ui'; -import { useForumData } from '@/context/ForumDataContext'; -import { isValidHTMLContent } from '@/utils/htmlValidator'; -import { renderContent } from '@/utils/quillUtils'; -import PaginatedSection from '@/views/cleanairforum/PaginatedSection'; -import SectionDisplay from '@/views/Forum/SectionDisplay'; - -const PartnersPage: React.FC = () => { - const { selectedEvent } = useForumData(); - if (!selectedEvent) return null; - - const conveningPartners = selectedEvent.partners - ?.filter((partner: any) => partner.category === 'Co-Convening Partner') - .map((partner: any) => ({ - id: partner.id, - logoUrl: partner.partner_logo_url, - })); - - const hostPartners = selectedEvent.partners - ?.filter((partner: any) => partner.category === 'Host Partner') - .map((partner: any) => ({ - id: partner.id, - logoUrl: partner.partner_logo_url, - })); - - const programPartners = selectedEvent.partners - ?.filter((partner: any) => partner.category === 'Program Partner') - .map((partner: any) => ({ - id: partner.id, - logoUrl: partner.partner_logo_url, - })); - - const fundingPartners = selectedEvent.partners - ?.filter((partner: any) => partner.category === 'Funding Partner') - .map((partner: any) => ({ - id: partner.id, - logoUrl: partner.partner_logo_url, - })); - - const mainPartnersHTML = renderContent(selectedEvent.partners_text_section); - const showMainPartners = isValidHTMLContent(mainPartnersHTML); - - const partnersSections = selectedEvent.sections?.filter((section: any) => { - if (!section.pages.includes('partners')) return false; - const sectionHTML = renderContent(section.content); - return isValidHTMLContent(sectionHTML); - }); - - return ( -
- {showMainPartners && ( -
-

Partners

-
-
- )} - - {partnersSections && partnersSections.length > 0 && ( - <> - {partnersSections.map((section: any) => ( - - ))} - - )} - - {conveningPartners && conveningPartners.length > 0 && ( - <> - -
-
-

- Convening partners and Collaborators -

-
- -
- - )} - - {hostPartners && hostPartners.length > 0 && ( - <> - -
-
-

- Host partners -

-
- -
- - )} - - {programPartners && programPartners.length > 0 && ( - <> - -
-
-

Exhibitors

-
- -
- - )} - - {fundingPartners && fundingPartners.length > 0 && ( - <> - -
-
-

- Funding Partners and Sponsors -

-
- -
- - )} -
- ); -}; - -export default PartnersPage; +'use client'; + +import DOMPurify from 'dompurify'; +import React from 'react'; + +import { Divider } from '@/components/ui'; +import { useForumData } from '@/context/ForumDataContext'; +import { isValidHTMLContent } from '@/utils/htmlValidator'; +import { renderContent } from '@/utils/quillUtils'; +import PaginatedSection from '@/views/cleanairforum/PaginatedSection'; +import SectionDisplay from '@/views/Forum/SectionDisplay'; + +const PartnersPage: React.FC = () => { + const { selectedEvent } = useForumData(); + if (!selectedEvent) return null; + + const conveningPartners = selectedEvent.partners + ?.filter((partner: any) => partner.category === 'Co-Convening Partner') + .map((partner: any) => ({ + id: partner.id, + logoUrl: partner.partner_logo_url, + })); + + const hostPartners = selectedEvent.partners + ?.filter((partner: any) => partner.category === 'Host Partner') + .map((partner: any) => ({ + id: partner.id, + logoUrl: partner.partner_logo_url, + })); + + const programPartners = selectedEvent.partners + ?.filter((partner: any) => partner.category === 'Program Partner') + .map((partner: any) => ({ + id: partner.id, + logoUrl: partner.partner_logo_url, + })); + + const fundingPartners = selectedEvent.partners + ?.filter((partner: any) => partner.category === 'Funding Partner') + .map((partner: any) => ({ + id: partner.id, + logoUrl: partner.partner_logo_url, + })); + + const mainPartnersHTML = renderContent(selectedEvent.partners_text_section); + const showMainPartners = isValidHTMLContent(mainPartnersHTML); + + const partnersSections = selectedEvent.sections?.filter((section: any) => { + if (!section.pages.includes('partners')) return false; + const sectionHTML = renderContent(section.content); + return isValidHTMLContent(sectionHTML); + }); + + return ( +
+ {showMainPartners && ( +
+

Partners

+
+
+ )} + + {partnersSections && partnersSections.length > 0 && ( + <> + {partnersSections.map((section: any) => ( + + ))} + + )} + + {conveningPartners && conveningPartners.length > 0 && ( + <> + +
+
+

+ Convening partners and Collaborators +

+
+ +
+ + )} + + {hostPartners && hostPartners.length > 0 && ( + <> + +
+
+

+ Host partners +

+
+ +
+ + )} + + {programPartners && programPartners.length > 0 && ( + <> + +
+
+

Exhibitors

+
+ +
+ + )} + + {fundingPartners && fundingPartners.length > 0 && ( + <> + +
+
+

+ Funding Partners and Sponsors +

+
+ +
+ + )} +
+ ); +}; + +export default PartnersPage; diff --git a/website2/src/app/clean-air-forum/program-committee/page.tsx b/website2/src/app/[locale]/clean-air-forum/program-committee/page.tsx similarity index 97% rename from website2/src/app/clean-air-forum/program-committee/page.tsx rename to website2/src/app/[locale]/clean-air-forum/program-committee/page.tsx index ab3f7d2b3d..56aa5e9fd0 100644 --- a/website2/src/app/clean-air-forum/program-committee/page.tsx +++ b/website2/src/app/[locale]/clean-air-forum/program-committee/page.tsx @@ -1,121 +1,121 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client'; - -import DOMPurify from 'dompurify'; -import React, { useMemo, useState } from 'react'; - -import { Divider, MemberCard, Pagination } from '@/components/ui/'; -import { useForumData } from '@/context/ForumDataContext'; -import { isValidHTMLContent } from '@/utils/htmlValidator'; -import { renderContent } from '@/utils/quillUtils'; -import SectionDisplay from '@/views/Forum/SectionDisplay'; - -const CommitteePage: React.FC = () => { - // Always call useForumData to get the selectedEvent. - const { selectedEvent } = useForumData(); - - // Instead of conditionally calling hooks based on selectedEvent, - // extract fallback values unconditionally. - const persons = selectedEvent?.persons || []; - const sections = selectedEvent?.sections || []; - const committeeText = selectedEvent?.committee_text_section || ''; - - // Local state for pagination. - const [currentPage, setCurrentPage] = useState(1); - const membersPerPage = 6; - - // Memoize committee members using a fallback empty array. - const committeeMembers = useMemo(() => { - return persons.filter( - (person: any) => - person.category === 'Committee Member' || - person.category === 'Committee Member and Key Note Speaker' || - person.category === 'Speaker and Committee Member', - ); - }, [persons]); - - // Calculate total pages. - const totalPages = useMemo(() => { - return Math.ceil(committeeMembers.length / membersPerPage); - }, [committeeMembers, membersPerPage]); - - // Get members for the current page. - const displayedMembers = useMemo(() => { - const startIdx = (currentPage - 1) * membersPerPage; - return committeeMembers.slice(startIdx, startIdx + membersPerPage); - }, [currentPage, committeeMembers, membersPerPage]); - - // Render main committee text. - const committeeHTML = renderContent(committeeText); - const showCommitteeMain = isValidHTMLContent(committeeHTML); - - // Filter extra sections assigned to the "committee" page. - const committeeSections = useMemo(() => { - return sections.filter((section: any) => { - if (!section.pages.includes('committee')) return false; - const sectionHTML = renderContent(section.content); - return isValidHTMLContent(sectionHTML); - }); - }, [sections]); - - const handlePageChange = (newPage: number) => setCurrentPage(newPage); - - // If selectedEvent is still not available, you might render a loading indicator. - if (!selectedEvent) { - return null; - } - - return ( -
- - - {/* Program Committee Text Section */} -
- {showCommitteeMain && ( - <> -

Program Committee

-
- - )} -
- - {/* Extra Committee Sections using SectionDisplay */} - {committeeSections.length > 0 && ( - <> - {committeeSections.map((section: any) => ( - - ))} - - )} - - {/* Member Cards Grid */} -
- {displayedMembers.map((person: any) => ( - - ))} -
- - {/* Pagination Component */} - {totalPages > 1 && ( -
- -
- )} -
- ); -}; - -export default CommitteePage; +/* eslint-disable react-hooks/exhaustive-deps */ +'use client'; + +import DOMPurify from 'dompurify'; +import React, { useMemo, useState } from 'react'; + +import { Divider, MemberCard, Pagination } from '@/components/ui/'; +import { useForumData } from '@/context/ForumDataContext'; +import { isValidHTMLContent } from '@/utils/htmlValidator'; +import { renderContent } from '@/utils/quillUtils'; +import SectionDisplay from '@/views/Forum/SectionDisplay'; + +const CommitteePage: React.FC = () => { + // Always call useForumData to get the selectedEvent. + const { selectedEvent } = useForumData(); + + // Instead of conditionally calling hooks based on selectedEvent, + // extract fallback values unconditionally. + const persons = selectedEvent?.persons || []; + const sections = selectedEvent?.sections || []; + const committeeText = selectedEvent?.committee_text_section || ''; + + // Local state for pagination. + const [currentPage, setCurrentPage] = useState(1); + const membersPerPage = 6; + + // Memoize committee members using a fallback empty array. + const committeeMembers = useMemo(() => { + return persons.filter( + (person: any) => + person.category === 'Committee Member' || + person.category === 'Committee Member and Key Note Speaker' || + person.category === 'Speaker and Committee Member', + ); + }, [persons]); + + // Calculate total pages. + const totalPages = useMemo(() => { + return Math.ceil(committeeMembers.length / membersPerPage); + }, [committeeMembers, membersPerPage]); + + // Get members for the current page. + const displayedMembers = useMemo(() => { + const startIdx = (currentPage - 1) * membersPerPage; + return committeeMembers.slice(startIdx, startIdx + membersPerPage); + }, [currentPage, committeeMembers, membersPerPage]); + + // Render main committee text. + const committeeHTML = renderContent(committeeText); + const showCommitteeMain = isValidHTMLContent(committeeHTML); + + // Filter extra sections assigned to the "committee" page. + const committeeSections = useMemo(() => { + return sections.filter((section: any) => { + if (!section.pages.includes('committee')) return false; + const sectionHTML = renderContent(section.content); + return isValidHTMLContent(sectionHTML); + }); + }, [sections]); + + const handlePageChange = (newPage: number) => setCurrentPage(newPage); + + // If selectedEvent is still not available, you might render a loading indicator. + if (!selectedEvent) { + return null; + } + + return ( +
+ + + {/* Program Committee Text Section */} +
+ {showCommitteeMain && ( + <> +

Program Committee

+
+ + )} +
+ + {/* Extra Committee Sections using SectionDisplay */} + {committeeSections.length > 0 && ( + <> + {committeeSections.map((section: any) => ( + + ))} + + )} + + {/* Member Cards Grid */} +
+ {displayedMembers.map((person: any) => ( + + ))} +
+ + {/* Pagination Component */} + {totalPages > 1 && ( +
+ +
+ )} +
+ ); +}; + +export default CommitteePage; diff --git a/website2/src/app/clean-air-forum/resources/page.tsx b/website2/src/app/[locale]/clean-air-forum/resources/page.tsx similarity index 96% rename from website2/src/app/clean-air-forum/resources/page.tsx rename to website2/src/app/[locale]/clean-air-forum/resources/page.tsx index a107048955..e356f3792b 100644 --- a/website2/src/app/clean-air-forum/resources/page.tsx +++ b/website2/src/app/[locale]/clean-air-forum/resources/page.tsx @@ -1,148 +1,148 @@ -'use client'; - -import React, { useState } from 'react'; -import { FaChevronDown, FaChevronUp, FaFilePdf } from 'react-icons/fa'; - -import { Divider } from '@/components/ui'; -import { useForumData } from '@/context/ForumDataContext'; - -const getFileNameFromUrl = (url: string | null | undefined): string | null => { - if (!url || typeof url !== 'string') { - console.error('Invalid URL:', url); - return null; - } - const segments = url.split('/'); - return segments.pop() || null; -}; - -const AccordionItem = ({ session, isOpen, toggleAccordion }: any) => { - return ( -
-
-

- {session.session_title} -

- {isOpen ? : } -
- {isOpen && ( -
- {session?.resource_files?.map((file: any) => ( -
-
-
-

- {file.resource_summary} -

- - - {getFileNameFromUrl(file.file_url)} - -
-
-
- ))} -
- )} -
- ); -}; - -const ResourcesPage: React.FC = () => { - const { selectedEvent } = useForumData(); - const [openAccordions, setOpenAccordions] = useState<{ - [resourceIndex: number]: { [sessionIndex: number]: boolean }; - }>({}); - const [allExpanded, setAllExpanded] = useState(false); - - if (!selectedEvent) { - return null; - } - - const handleToggleAccordion = ( - resourceIndex: number, - sessionIndex: number, - ) => { - setOpenAccordions((prevState) => ({ - ...prevState, - [resourceIndex]: { - ...prevState[resourceIndex], - [sessionIndex]: !prevState[resourceIndex]?.[sessionIndex], - }, - })); - }; - - const handleExpandAll = () => { - setAllExpanded(true); - const expandedAccordions: any = {}; - selectedEvent.forum_resources?.forEach( - (resource: any, resourceIndex: number) => { - expandedAccordions[resourceIndex] = {}; - resource.resource_sessions?.forEach((_: any, sessionIndex: number) => { - expandedAccordions[resourceIndex][sessionIndex] = true; - }); - }, - ); - setOpenAccordions(expandedAccordions); - }; - - const handleCollapseAll = () => { - setAllExpanded(false); - setOpenAccordions({}); - }; - - return ( -
-
- - -
- - {selectedEvent.forum_resources?.map( - (resource: any, resourceIndex: number) => ( -
-

- {resource.resource_title} -

- {resource.resource_sessions?.map( - (session: any, sessionIndex: number) => ( - - handleToggleAccordion(resourceIndex, sessionIndex) - } - /> - ), - )} - -
- ), - )} -
- ); -}; - -export default ResourcesPage; +'use client'; + +import React, { useState } from 'react'; +import { FaChevronDown, FaChevronUp, FaFilePdf } from 'react-icons/fa'; + +import { Divider } from '@/components/ui'; +import { useForumData } from '@/context/ForumDataContext'; + +const getFileNameFromUrl = (url: string | null | undefined): string | null => { + if (!url || typeof url !== 'string') { + console.error('Invalid URL:', url); + return null; + } + const segments = url.split('/'); + return segments.pop() || null; +}; + +const AccordionItem = ({ session, isOpen, toggleAccordion }: any) => { + return ( +
+
+

+ {session.session_title} +

+ {isOpen ? : } +
+ {isOpen && ( +
+ {session?.resource_files?.map((file: any) => ( +
+
+
+

+ {file.resource_summary} +

+ + + {getFileNameFromUrl(file.file_url)} + +
+
+
+ ))} +
+ )} +
+ ); +}; + +const ResourcesPage: React.FC = () => { + const { selectedEvent } = useForumData(); + const [openAccordions, setOpenAccordions] = useState<{ + [resourceIndex: number]: { [sessionIndex: number]: boolean }; + }>({}); + const [allExpanded, setAllExpanded] = useState(false); + + if (!selectedEvent) { + return null; + } + + const handleToggleAccordion = ( + resourceIndex: number, + sessionIndex: number, + ) => { + setOpenAccordions((prevState) => ({ + ...prevState, + [resourceIndex]: { + ...prevState[resourceIndex], + [sessionIndex]: !prevState[resourceIndex]?.[sessionIndex], + }, + })); + }; + + const handleExpandAll = () => { + setAllExpanded(true); + const expandedAccordions: any = {}; + selectedEvent.forum_resources?.forEach( + (resource: any, resourceIndex: number) => { + expandedAccordions[resourceIndex] = {}; + resource.resource_sessions?.forEach((_: any, sessionIndex: number) => { + expandedAccordions[resourceIndex][sessionIndex] = true; + }); + }, + ); + setOpenAccordions(expandedAccordions); + }; + + const handleCollapseAll = () => { + setAllExpanded(false); + setOpenAccordions({}); + }; + + return ( +
+
+ + +
+ + {selectedEvent.forum_resources?.map( + (resource: any, resourceIndex: number) => ( +
+

+ {resource.resource_title} +

+ {resource.resource_sessions?.map( + (session: any, sessionIndex: number) => ( + + handleToggleAccordion(resourceIndex, sessionIndex) + } + /> + ), + )} + +
+ ), + )} +
+ ); +}; + +export default ResourcesPage; diff --git a/website2/src/app/clean-air-forum/sessions/page.tsx b/website2/src/app/[locale]/clean-air-forum/sessions/page.tsx similarity index 96% rename from website2/src/app/clean-air-forum/sessions/page.tsx rename to website2/src/app/[locale]/clean-air-forum/sessions/page.tsx index 010ab091cf..9348960694 100644 --- a/website2/src/app/clean-air-forum/sessions/page.tsx +++ b/website2/src/app/[locale]/clean-air-forum/sessions/page.tsx @@ -1,158 +1,158 @@ -'use client'; - -import { format } from 'date-fns'; -import DOMPurify from 'dompurify'; -import React, { useState } from 'react'; -import { FaChevronDown, FaChevronUp } from 'react-icons/fa'; - -import { Divider } from '@/components/ui'; -import { useForumData } from '@/context/ForumDataContext'; -import { isValidHTMLContent } from '@/utils/htmlValidator'; -import { renderContent } from '@/utils/quillUtils'; -import SectionDisplay from '@/views/Forum/SectionDisplay'; - -interface AccordionItemProps { - title: string; - subText: string; - sessions: any[]; - isOpen: boolean; - onToggle: () => void; -} - -const AccordionItem: React.FC = ({ - title, - subText, - sessions, - isOpen, - onToggle, -}) => { - const formatTime = (time: string) => { - try { - return format(new Date(`1970-01-01T${time}Z`), 'p'); - } catch (error) { - console.error(error); - return time; - } - }; - - return ( -
-
-
-

{title}

-
-
- {isOpen ? : } -
- {isOpen && ( -
- {sessions?.map((item: any, index: number) => ( -
- -
-
- {formatTime(item.start_time)} -
-
-

{item.session_title}

-
-
-
-
- ))} -
- )} -
- ); -}; - -const ProgramsPage: React.FC = () => { - const { selectedEvent } = useForumData(); - const [openAccordion, setOpenAccordion] = useState(null); - - if (!selectedEvent) { - return null; - } - - const scheduleHTML = renderContent(selectedEvent.schedule_details); - const showSchedule = isValidHTMLContent(scheduleHTML); - - const registrationHTML = renderContent(selectedEvent.registration_details); - const showRegistration = isValidHTMLContent(registrationHTML); - - const sessionSections = selectedEvent.sections?.filter((section: any) => { - if (!section.pages.includes('session')) return false; - const sectionHTML = renderContent(section.content); - return isValidHTMLContent(sectionHTML); - }); - - const handleToggle = (id: string) => { - setOpenAccordion(openAccordion === id ? null : id); - }; - - return ( -
- {showSchedule && ( -
-

Schedule

-
-
- )} - - {sessionSections && sessionSections.length > 0 && ( - <> - {sessionSections.map((section: any) => ( - - ))} - - )} - - <> - {selectedEvent.programs?.map((program: any) => ( - handleToggle(program.id)} - /> - ))} - - - {showRegistration && ( -
- -
-
-

Registration

-
-
-
-
- )} -
- ); -}; - -export default ProgramsPage; +'use client'; + +import { format } from 'date-fns'; +import DOMPurify from 'dompurify'; +import React, { useState } from 'react'; +import { FaChevronDown, FaChevronUp } from 'react-icons/fa'; + +import { Divider } from '@/components/ui'; +import { useForumData } from '@/context/ForumDataContext'; +import { isValidHTMLContent } from '@/utils/htmlValidator'; +import { renderContent } from '@/utils/quillUtils'; +import SectionDisplay from '@/views/Forum/SectionDisplay'; + +interface AccordionItemProps { + title: string; + subText: string; + sessions: any[]; + isOpen: boolean; + onToggle: () => void; +} + +const AccordionItem: React.FC = ({ + title, + subText, + sessions, + isOpen, + onToggle, +}) => { + const formatTime = (time: string) => { + try { + return format(new Date(`1970-01-01T${time}Z`), 'p'); + } catch (error) { + console.error(error); + return time; + } + }; + + return ( +
+
+
+

{title}

+
+
+ {isOpen ? : } +
+ {isOpen && ( +
+ {sessions?.map((item: any, index: number) => ( +
+ +
+
+ {formatTime(item.start_time)} +
+
+

{item.session_title}

+
+
+
+
+ ))} +
+ )} +
+ ); +}; + +const ProgramsPage: React.FC = () => { + const { selectedEvent } = useForumData(); + const [openAccordion, setOpenAccordion] = useState(null); + + if (!selectedEvent) { + return null; + } + + const scheduleHTML = renderContent(selectedEvent.schedule_details); + const showSchedule = isValidHTMLContent(scheduleHTML); + + const registrationHTML = renderContent(selectedEvent.registration_details); + const showRegistration = isValidHTMLContent(registrationHTML); + + const sessionSections = selectedEvent.sections?.filter((section: any) => { + if (!section.pages.includes('session')) return false; + const sectionHTML = renderContent(section.content); + return isValidHTMLContent(sectionHTML); + }); + + const handleToggle = (id: string) => { + setOpenAccordion(openAccordion === id ? null : id); + }; + + return ( +
+ {showSchedule && ( +
+

Schedule

+
+
+ )} + + {sessionSections && sessionSections.length > 0 && ( + <> + {sessionSections.map((section: any) => ( + + ))} + + )} + + <> + {selectedEvent.programs?.map((program: any) => ( + handleToggle(program.id)} + /> + ))} + + + {showRegistration && ( +
+ +
+
+

Registration

+
+
+
+
+ )} +
+ ); +}; + +export default ProgramsPage; diff --git a/website2/src/app/clean-air-forum/speakers/page.tsx b/website2/src/app/[locale]/clean-air-forum/speakers/page.tsx similarity index 97% rename from website2/src/app/clean-air-forum/speakers/page.tsx rename to website2/src/app/[locale]/clean-air-forum/speakers/page.tsx index af98135d6f..22e11e9fcb 100644 --- a/website2/src/app/clean-air-forum/speakers/page.tsx +++ b/website2/src/app/[locale]/clean-air-forum/speakers/page.tsx @@ -1,147 +1,147 @@ -'use client'; - -import DOMPurify from 'dompurify'; -import React, { useState } from 'react'; - -import { Divider, MemberCard, Pagination } from '@/components/ui/'; -import { useForumData } from '@/context/ForumDataContext'; -import { isValidHTMLContent } from '@/utils/htmlValidator'; -import { renderContent } from '@/utils/quillUtils'; -import SectionDisplay from '@/views/Forum/SectionDisplay'; - -const SpeakersPage: React.FC = () => { - // Now we use the selectedEvent from context - const { selectedEvent } = useForumData(); - const membersPerPage = 6; - const [currentKeyNotePage, setCurrentKeyNotePage] = useState(1); - const [currentSpeakersPage, setCurrentSpeakersPage] = useState(1); - - if (!selectedEvent) { - return null; - } - - // Filter keynote speakers and speakers from selectedEvent.persons. - // (Adjust your filtering logic as needed.) - const keyNoteSpeakers = - selectedEvent.persons?.filter( - (person: any) => - person.category === 'Key Note Speaker' || - person.category === 'Committee Member and Key Note Speaker', - ) || []; - const speakers = - selectedEvent.persons?.filter( - (person: any) => - person.category === 'Speaker' || - person.category === 'Speaker and Committee Member', - ) || []; - - // Pagination calculations for Keynote Speakers. - const totalKeyNotePages = Math.ceil(keyNoteSpeakers.length / membersPerPage); - const startKeyNoteIdx = (currentKeyNotePage - 1) * membersPerPage; - const displayedKeyNoteSpeakers = keyNoteSpeakers.slice( - startKeyNoteIdx, - startKeyNoteIdx + membersPerPage, - ); - - // Pagination calculations for Speakers. - const totalSpeakersPages = Math.ceil(speakers.length / membersPerPage); - const startSpeakersIdx = (currentSpeakersPage - 1) * membersPerPage; - const displayedSpeakers = speakers.slice( - startSpeakersIdx, - startSpeakersIdx + membersPerPage, - ); - - // Handlers for page changes. - const handleKeyNotePageChange = (newPage: number) => - setCurrentKeyNotePage(newPage); - const handleSpeakersPageChange = (newPage: number) => - setCurrentSpeakersPage(newPage); - - // Validate the main speakers text section. - const mainSpeakersHTML = renderContent(selectedEvent.speakers_text_section); - const showMainSpeakers = isValidHTMLContent(mainSpeakersHTML); - - // Filter extra sections assigned to the "speakers" page. - const speakersExtraSections = selectedEvent.sections?.filter( - (section: any) => { - if (!section.pages.includes('speakers')) return false; - const sectionHTML = renderContent(section.content); - return isValidHTMLContent(sectionHTML); - }, - ); - - return ( -
- - - {/* Speakers Text Section */} - {showMainSpeakers && ( -
-
-
- )} - - {/* Keynote Speakers Section */} -

Keynote Speakers

- -
- {displayedKeyNoteSpeakers.map((person: any) => ( - - ))} -
- {totalKeyNotePages > 1 && ( -
- -
- )} - - - - {/* Speakers Section */} -

Speakers

-
- {displayedSpeakers.map((person: any) => ( - - ))} -
- {totalSpeakersPages > 1 && ( -
- -
- )} - - {/* Extra Speakers Sections */} - {speakersExtraSections && speakersExtraSections.length > 0 && ( - <> - {speakersExtraSections.map((section: any) => ( - - ))} - - )} -
- ); -}; - -export default SpeakersPage; +'use client'; + +import DOMPurify from 'dompurify'; +import React, { useState } from 'react'; + +import { Divider, MemberCard, Pagination } from '@/components/ui/'; +import { useForumData } from '@/context/ForumDataContext'; +import { isValidHTMLContent } from '@/utils/htmlValidator'; +import { renderContent } from '@/utils/quillUtils'; +import SectionDisplay from '@/views/Forum/SectionDisplay'; + +const SpeakersPage: React.FC = () => { + // Now we use the selectedEvent from context + const { selectedEvent } = useForumData(); + const membersPerPage = 6; + const [currentKeyNotePage, setCurrentKeyNotePage] = useState(1); + const [currentSpeakersPage, setCurrentSpeakersPage] = useState(1); + + if (!selectedEvent) { + return null; + } + + // Filter keynote speakers and speakers from selectedEvent.persons. + // (Adjust your filtering logic as needed.) + const keyNoteSpeakers = + selectedEvent.persons?.filter( + (person: any) => + person.category === 'Key Note Speaker' || + person.category === 'Committee Member and Key Note Speaker', + ) || []; + const speakers = + selectedEvent.persons?.filter( + (person: any) => + person.category === 'Speaker' || + person.category === 'Speaker and Committee Member', + ) || []; + + // Pagination calculations for Keynote Speakers. + const totalKeyNotePages = Math.ceil(keyNoteSpeakers.length / membersPerPage); + const startKeyNoteIdx = (currentKeyNotePage - 1) * membersPerPage; + const displayedKeyNoteSpeakers = keyNoteSpeakers.slice( + startKeyNoteIdx, + startKeyNoteIdx + membersPerPage, + ); + + // Pagination calculations for Speakers. + const totalSpeakersPages = Math.ceil(speakers.length / membersPerPage); + const startSpeakersIdx = (currentSpeakersPage - 1) * membersPerPage; + const displayedSpeakers = speakers.slice( + startSpeakersIdx, + startSpeakersIdx + membersPerPage, + ); + + // Handlers for page changes. + const handleKeyNotePageChange = (newPage: number) => + setCurrentKeyNotePage(newPage); + const handleSpeakersPageChange = (newPage: number) => + setCurrentSpeakersPage(newPage); + + // Validate the main speakers text section. + const mainSpeakersHTML = renderContent(selectedEvent.speakers_text_section); + const showMainSpeakers = isValidHTMLContent(mainSpeakersHTML); + + // Filter extra sections assigned to the "speakers" page. + const speakersExtraSections = selectedEvent.sections?.filter( + (section: any) => { + if (!section.pages.includes('speakers')) return false; + const sectionHTML = renderContent(section.content); + return isValidHTMLContent(sectionHTML); + }, + ); + + return ( +
+ + + {/* Speakers Text Section */} + {showMainSpeakers && ( +
+
+
+ )} + + {/* Keynote Speakers Section */} +

Keynote Speakers

+ +
+ {displayedKeyNoteSpeakers.map((person: any) => ( + + ))} +
+ {totalKeyNotePages > 1 && ( +
+ +
+ )} + + + + {/* Speakers Section */} +

Speakers

+
+ {displayedSpeakers.map((person: any) => ( + + ))} +
+ {totalSpeakersPages > 1 && ( +
+ +
+ )} + + {/* Extra Speakers Sections */} + {speakersExtraSections && speakersExtraSections.length > 0 && ( + <> + {speakersExtraSections.map((section: any) => ( + + ))} + + )} +
+ ); +}; + +export default SpeakersPage; diff --git a/website2/src/app/clean-air-forum/sponsorships/page.tsx b/website2/src/app/[locale]/clean-air-forum/sponsorships/page.tsx similarity index 96% rename from website2/src/app/clean-air-forum/sponsorships/page.tsx rename to website2/src/app/[locale]/clean-air-forum/sponsorships/page.tsx index 28e2a89534..ecd582e6ff 100644 --- a/website2/src/app/clean-air-forum/sponsorships/page.tsx +++ b/website2/src/app/[locale]/clean-air-forum/sponsorships/page.tsx @@ -1,82 +1,82 @@ -'use client'; - -import DOMPurify from 'dompurify'; -import React from 'react'; - -import { Divider } from '@/components/ui'; -import { useForumData } from '@/context/ForumDataContext'; -import { renderContent } from '@/utils/quillUtils'; -import PaginatedSection from '@/views/cleanairforum/PaginatedSection'; -import SectionDisplay from '@/views/Forum/SectionDisplay'; - -const SponsorshipPage: React.FC = () => { - const { selectedEvent } = useForumData(); - if (!selectedEvent) return null; - - const sponsorPartner = selectedEvent.partners - ?.filter((partner: any) => partner.category === 'Sponsor Partner') - .map((partner: any) => ({ - id: partner.id, - logoUrl: partner.partner_logo_url, - })); - - const sponsorshipSections = selectedEvent.sections?.filter((section: any) => { - if (!section.pages.includes('sponsorships')) return false; - const html = renderContent(section.content); - return html.trim().length > 0; - }); - - const mainSponsorshipHTML = renderContent( - selectedEvent.sponsorship_opportunities_partners, - ); - const showMainSponsorship = mainSponsorshipHTML.trim().length > 0; - - return ( -
- {showMainSponsorship && ( -
- -
-

Sponsorship opportunities

- -
-
-
- )} - - {sponsorshipSections && sponsorshipSections.length > 0 && ( - <> - {sponsorshipSections.map((section: any) => ( - - ))} - - )} - - {sponsorPartner && sponsorPartner.length > 0 && ( - <> - -
-
-
-

- Sponsors -

-
- -
-
- - )} -
- ); -}; - -export default SponsorshipPage; +'use client'; + +import DOMPurify from 'dompurify'; +import React from 'react'; + +import { Divider } from '@/components/ui'; +import { useForumData } from '@/context/ForumDataContext'; +import { renderContent } from '@/utils/quillUtils'; +import PaginatedSection from '@/views/cleanairforum/PaginatedSection'; +import SectionDisplay from '@/views/Forum/SectionDisplay'; + +const SponsorshipPage: React.FC = () => { + const { selectedEvent } = useForumData(); + if (!selectedEvent) return null; + + const sponsorPartner = selectedEvent.partners + ?.filter((partner: any) => partner.category === 'Sponsor Partner') + .map((partner: any) => ({ + id: partner.id, + logoUrl: partner.partner_logo_url, + })); + + const sponsorshipSections = selectedEvent.sections?.filter((section: any) => { + if (!section.pages.includes('sponsorships')) return false; + const html = renderContent(section.content); + return html.trim().length > 0; + }); + + const mainSponsorshipHTML = renderContent( + selectedEvent.sponsorship_opportunities_partners, + ); + const showMainSponsorship = mainSponsorshipHTML.trim().length > 0; + + return ( +
+ {showMainSponsorship && ( +
+ +
+

Sponsorship opportunities

+ +
+
+
+ )} + + {sponsorshipSections && sponsorshipSections.length > 0 && ( + <> + {sponsorshipSections.map((section: any) => ( + + ))} + + )} + + {sponsorPartner && sponsorPartner.length > 0 && ( + <> + +
+
+
+

+ Sponsors +

+
+ +
+
+ + )} +
+ ); +}; + +export default SponsorshipPage; diff --git a/website2/src/app/clean-air-network/CleanAirPage.tsx b/website2/src/app/[locale]/clean-air-network/CleanAirPage.tsx similarity index 97% rename from website2/src/app/clean-air-network/CleanAirPage.tsx rename to website2/src/app/[locale]/clean-air-network/CleanAirPage.tsx index cd9601bc89..4afd7a96a0 100644 --- a/website2/src/app/clean-air-network/CleanAirPage.tsx +++ b/website2/src/app/[locale]/clean-air-network/CleanAirPage.tsx @@ -1,196 +1,196 @@ -'use client'; -import Image from 'next/image'; -import React from 'react'; - -import ContentSection from '@/views/cleanairforum/ContentSection'; -import FeaturedEvent from '@/views/cleanairforum/FeaturedEvent'; - -const CleanAirPage = () => { - const goals = [ - { - id: 1, - title: 'Enhancing Regional Capacity', - description: - 'Dedicated to improving capacity in air quality monitoring, modeling, data management, and access through scaling up of ongoing localized initiatives in African Cities.', - icon: 'https://res.cloudinary.com/dbibjvyhm/image/upload/v1728132391/website/cleanAirForum/images/goal1_xebb2a.webp', - }, - { - id: 2, - title: 'Collaboration and Awareness', - description: - 'Committed to fostering a deeper understanding, awareness, and appreciation of air quality issues through evidence-informed and participatory advocacy, and knowledge sharing.', - icon: 'https://res.cloudinary.com/dbibjvyhm/image/upload/v1728132390/website/cleanAirForum/images/goal2_hwv6m6.webp', - }, - { - id: 3, - title: 'Clean Air Solutions for Cities', - description: - 'CLEAN-Air network is a nexus for developing tangible and contextual clean air solutions and frameworks for African cities.', - icon: 'https://res.cloudinary.com/dbibjvyhm/image/upload/v1728132390/website/cleanAirForum/images/goal3_y9cj9l.webp', - }, - ]; - - return ( -
-
- - - An African-led, multi-regional network - -
- bringing together a community of practice for air quality - solutions and air quality management across Africa. -

- } - buttonText="Join the Network" - buttonLink="https://docs.google.com/forms/d/e/1FAIpQLScIPz7VrhfO2ifMI0dPWIQRiGQ9y30LoKUCT-DDyorS7sAKUA/viewform" - titleClassName="text-4xl lg:text-[56px] leading-[1.1]" - contentClassName="text-left space-y-4" - buttonClassName="rounded-none" - imgSrc="https://res.cloudinary.com/dbibjvyhm/image/upload/v1728132390/website/cleanAirForum/images/section1_usfuoj.webp" - imgAlt="Mission Image" - reverse={true} - /> -
- -
- {/* Text Content */} -
-

- CLEAN-Air, is - an acronym coined from -

-

- “Championing Liveable urban Environments through African Networks - for Air” -

-

- The network brings together stakeholders and researchers in air - quality management to share best practices and knowledge on - developing and implementing air quality management solutions in - African cities. -

-

- Are you an organization or individual interested in air quality in - Africa? - - Join the network - -

-
- - {/* Background Image */} -
- Urban Scene -
-
- -
- - To strengthen regional networks for sustained partnerships and - enable partners to co-develop solutions that enhance the capacity - for air quality monitoring, modelling and management across cities - in Africa. -

- } - contentClassName="text-left space-y-4" - buttonClassName="hidden" - imgSrc="https://res.cloudinary.com/dbibjvyhm/image/upload/v1728132391/website/cleanAirForum/images/section3_vgzcbs.webp" - imgAlt="Mission Image" - reverse={true} - /> -
- -
- -

- The network comprises a diverse stakeholder landscape including - research organisations, city and national governments, the - private sector, development partners, and individuals who are - championing the air quality agenda in African cities. -

-

- Are you an organization or individual interested in air quality - in Africa? We welcome you to join the CLEAN-Air Network. -

-
- } - contentClassName="text-left space-y-4" - buttonClassName="hidden" - imgSrc="https://res.cloudinary.com/dbibjvyhm/image/upload/v1728132391/website/cleanAirForum/images/section4_kudfs4.webp" - imgAlt="Synergy Image" - reverse={false} - /> -
- -
-
-
- - Goals - -

- CLEAN Air Goals -

-
- -
- {goals.map((goal) => ( -
- {/* Icon Section */} -
- {goal.title} -
- - {/* Content Section */} -
-

- {goal.title} -

-

- {goal.description} -

-
-
- ))} -
-
-
- - -
- ); -}; - -export default CleanAirPage; +'use client'; +import Image from 'next/image'; +import React from 'react'; + +import ContentSection from '@/views/cleanairforum/ContentSection'; +import FeaturedEvent from '@/views/cleanairforum/FeaturedEvent'; + +const CleanAirPage = () => { + const goals = [ + { + id: 1, + title: 'Enhancing Regional Capacity', + description: + 'Dedicated to improving capacity in air quality monitoring, modeling, data management, and access through scaling up of ongoing localized initiatives in African Cities.', + icon: 'https://res.cloudinary.com/dbibjvyhm/image/upload/v1728132391/website/cleanAirForum/images/goal1_xebb2a.webp', + }, + { + id: 2, + title: 'Collaboration and Awareness', + description: + 'Committed to fostering a deeper understanding, awareness, and appreciation of air quality issues through evidence-informed and participatory advocacy, and knowledge sharing.', + icon: 'https://res.cloudinary.com/dbibjvyhm/image/upload/v1728132390/website/cleanAirForum/images/goal2_hwv6m6.webp', + }, + { + id: 3, + title: 'Clean Air Solutions for Cities', + description: + 'CLEAN-Air network is a nexus for developing tangible and contextual clean air solutions and frameworks for African cities.', + icon: 'https://res.cloudinary.com/dbibjvyhm/image/upload/v1728132390/website/cleanAirForum/images/goal3_y9cj9l.webp', + }, + ]; + + return ( +
+
+ + + An African-led, multi-regional network + +
+ bringing together a community of practice for air quality + solutions and air quality management across Africa. +

+ } + buttonText="Join the Network" + buttonLink="https://docs.google.com/forms/d/e/1FAIpQLScIPz7VrhfO2ifMI0dPWIQRiGQ9y30LoKUCT-DDyorS7sAKUA/viewform" + titleClassName="text-4xl lg:text-[56px] leading-[1.1]" + contentClassName="text-left space-y-4" + buttonClassName="rounded-none" + imgSrc="https://res.cloudinary.com/dbibjvyhm/image/upload/v1728132390/website/cleanAirForum/images/section1_usfuoj.webp" + imgAlt="Mission Image" + reverse={true} + /> +
+ +
+ {/* Text Content */} +
+

+ CLEAN-Air, is + an acronym coined from +

+

+ “Championing Liveable urban Environments through African Networks + for Air” +

+

+ The network brings together stakeholders and researchers in air + quality management to share best practices and knowledge on + developing and implementing air quality management solutions in + African cities. +

+

+ Are you an organization or individual interested in air quality in + Africa? + + Join the network + +

+
+ + {/* Background Image */} +
+ Urban Scene +
+
+ +
+ + To strengthen regional networks for sustained partnerships and + enable partners to co-develop solutions that enhance the capacity + for air quality monitoring, modelling and management across cities + in Africa. +

+ } + contentClassName="text-left space-y-4" + buttonClassName="hidden" + imgSrc="https://res.cloudinary.com/dbibjvyhm/image/upload/v1728132391/website/cleanAirForum/images/section3_vgzcbs.webp" + imgAlt="Mission Image" + reverse={true} + /> +
+ +
+ +

+ The network comprises a diverse stakeholder landscape including + research organisations, city and national governments, the + private sector, development partners, and individuals who are + championing the air quality agenda in African cities. +

+

+ Are you an organization or individual interested in air quality + in Africa? We welcome you to join the CLEAN-Air Network. +

+
+ } + contentClassName="text-left space-y-4" + buttonClassName="hidden" + imgSrc="https://res.cloudinary.com/dbibjvyhm/image/upload/v1728132391/website/cleanAirForum/images/section4_kudfs4.webp" + imgAlt="Synergy Image" + reverse={false} + /> +
+ +
+
+
+ + Goals + +

+ CLEAN Air Goals +

+
+ +
+ {goals.map((goal) => ( +
+ {/* Icon Section */} +
+ {goal.title} +
+ + {/* Content Section */} +
+

+ {goal.title} +

+

+ {goal.description} +

+
+
+ ))} +
+
+
+ + +
+ ); +}; + +export default CleanAirPage; diff --git a/website2/src/app/clean-air-network/events/[id]/page.tsx b/website2/src/app/[locale]/clean-air-network/events/[id]/page.tsx similarity index 94% rename from website2/src/app/clean-air-network/events/[id]/page.tsx rename to website2/src/app/[locale]/clean-air-network/events/[id]/page.tsx index 6b46e523ca..42882748a6 100644 --- a/website2/src/app/clean-air-network/events/[id]/page.tsx +++ b/website2/src/app/[locale]/clean-air-network/events/[id]/page.tsx @@ -1,13 +1,13 @@ -import React from 'react'; - -import SingleEvent from '@/views/cleanairforum/events/SingleEvent'; - -const page = ({ params }: { params: any }) => { - return ( -
- -
- ); -}; - -export default page; +import React from 'react'; + +import SingleEvent from '@/views/cleanairforum/events/SingleEvent'; + +const page = ({ params }: { params: any }) => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/website2/src/app/clean-air-network/events/page.tsx b/website2/src/app/[locale]/clean-air-network/events/page.tsx similarity index 93% rename from website2/src/app/clean-air-network/events/page.tsx rename to website2/src/app/[locale]/clean-air-network/events/page.tsx index 1033d25b12..82217482ec 100644 --- a/website2/src/app/clean-air-network/events/page.tsx +++ b/website2/src/app/[locale]/clean-air-network/events/page.tsx @@ -1,11 +1,11 @@ -import EventsPage from '@/views/cleanairforum/events/EventsPage'; - -const page = () => { - return ( -
- -
- ); -}; - -export default page; +import EventsPage from '@/views/cleanairforum/events/EventsPage'; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/website2/src/app/clean-air-network/layout.tsx b/website2/src/app/[locale]/clean-air-network/layout.tsx similarity index 96% rename from website2/src/app/clean-air-network/layout.tsx rename to website2/src/app/[locale]/clean-air-network/layout.tsx index 48a120c057..51ed1756ba 100644 --- a/website2/src/app/clean-air-network/layout.tsx +++ b/website2/src/app/[locale]/clean-air-network/layout.tsx @@ -1,42 +1,42 @@ -import { Metadata } from 'next'; -import React from 'react'; - -import ActionButtons2 from '@/components/layouts/ActionButtons2'; -import Footer from '@/components/layouts/Footer'; -import Navbar from '@/components/layouts/Navbar'; - -export const metadata: Metadata = { - title: 'Clean Air Network | AirQo Africa', - description: - 'Join the Clean Air Network by AirQo, connecting stakeholders and promoting actions to improve air quality across Africa. Explore events, resources, and membership opportunities.', - keywords: - 'Clean Air Network, AirQo Africa, air quality network, air quality stakeholders, air pollution, environmental health, clean air Africa, air quality events, air quality resources, air quality membership', -}; - -type CleanAirLayoutProps = { - children: React.ReactNode; -}; - -const CleanAirLayout: React.FC = ({ children }) => { - return ( -
- {/* Navbar */} - - - {/* Main Content */} -
{children}
- - {/* Action Buttons Section */} -
- -
- - {/* Footer */} -
-
-
-
- ); -}; - -export default CleanAirLayout; +import { Metadata } from 'next'; +import React from 'react'; + +import ActionButtons2 from '@/components/layouts/ActionButtons2'; +import Footer from '@/components/layouts/Footer'; +import Navbar from '@/components/layouts/Navbar'; + +export const metadata: Metadata = { + title: 'Clean Air Network | AirQo Africa', + description: + 'Join the Clean Air Network by AirQo, connecting stakeholders and promoting actions to improve air quality across Africa. Explore events, resources, and membership opportunities.', + keywords: + 'Clean Air Network, AirQo Africa, air quality network, air quality stakeholders, air pollution, environmental health, clean air Africa, air quality events, air quality resources, air quality membership', +}; + +type CleanAirLayoutProps = { + children: React.ReactNode; +}; + +const CleanAirLayout: React.FC = ({ children }) => { + return ( +
+ {/* Navbar */} + + + {/* Main Content */} +
{children}
+ + {/* Action Buttons Section */} +
+ +
+ + {/* Footer */} +
+
+
+
+ ); +}; + +export default CleanAirLayout; diff --git a/website2/src/app/clean-air-network/membership/page.tsx b/website2/src/app/[locale]/clean-air-network/membership/page.tsx similarity index 94% rename from website2/src/app/clean-air-network/membership/page.tsx rename to website2/src/app/[locale]/clean-air-network/membership/page.tsx index ee1803b08d..608de96d3a 100644 --- a/website2/src/app/clean-air-network/membership/page.tsx +++ b/website2/src/app/[locale]/clean-air-network/membership/page.tsx @@ -1,11 +1,11 @@ -import MemberPage from '@/views/cleanairforum/membership/MemberPage'; - -const page = () => { - return ( -
- -
- ); -}; - -export default page; +import MemberPage from '@/views/cleanairforum/membership/MemberPage'; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/website2/src/app/clean-air-network/page.tsx b/website2/src/app/[locale]/clean-air-network/page.tsx similarity index 93% rename from website2/src/app/clean-air-network/page.tsx rename to website2/src/app/[locale]/clean-air-network/page.tsx index b7a801b61d..c39ef20a87 100644 --- a/website2/src/app/clean-air-network/page.tsx +++ b/website2/src/app/[locale]/clean-air-network/page.tsx @@ -1,13 +1,13 @@ -import React from 'react'; - -import CleanAirPage from './CleanAirPage'; - -const page = () => { - return ( -
- -
- ); -}; - -export default page; +import React from 'react'; + +import CleanAirPage from './CleanAirPage'; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/website2/src/app/clean-air-network/resources/page.tsx b/website2/src/app/[locale]/clean-air-network/resources/page.tsx similarity index 94% rename from website2/src/app/clean-air-network/resources/page.tsx rename to website2/src/app/[locale]/clean-air-network/resources/page.tsx index 805d6b0e40..b8ef31e257 100644 --- a/website2/src/app/clean-air-network/resources/page.tsx +++ b/website2/src/app/[locale]/clean-air-network/resources/page.tsx @@ -1,11 +1,11 @@ -import ResourcePage from '@/views/cleanairforum/resources/ResourcePage'; - -const page = () => { - return ( -
- -
- ); -}; - -export default page; +import ResourcePage from '@/views/cleanairforum/resources/ResourcePage'; + +const page = () => { + return ( +
+ +
+ ); +}; + +export default page; diff --git a/website2/src/app/contact/ContactPage.tsx b/website2/src/app/[locale]/contact/ContactPage.tsx similarity index 96% rename from website2/src/app/contact/ContactPage.tsx rename to website2/src/app/[locale]/contact/ContactPage.tsx index 66a8263fd8..37f1c3638b 100644 --- a/website2/src/app/contact/ContactPage.tsx +++ b/website2/src/app/[locale]/contact/ContactPage.tsx @@ -1,116 +1,116 @@ -'use client'; -import { motion } from 'framer-motion'; -import { useRouter } from 'next/navigation'; -import React from 'react'; -import { FiDatabase, FiMessageCircle, FiStar, FiTablet } from 'react-icons/fi'; - -const ContactPage: React.FC = () => { - const router = useRouter(); - - const handleButtonClick = (detail: string) => { - router.push( - `/contact/form?category=${detail.toLowerCase().replace(/ /g, '-')}`, - ); - }; - - // Animation variants for the container - const containerVariants = { - hidden: { opacity: 0, y: 50 }, - visible: { - opacity: 1, - y: 0, - transition: { - duration: 0.6, - ease: 'easeOut', - staggerChildren: 0.2, - }, - }, - }; - - // Animation variants for each item - const itemVariants = { - hidden: { opacity: 0, y: 20 }, - visible: { - opacity: 1, - y: 0, - transition: { duration: 0.5, ease: 'easeOut' }, - }, - }; - - return ( -
- {/* Contact Information Section */} -
- -

Get in touch

-

Makerere University

-

- Software Systems Centre, Block B, Level 3, College of Computing and - Information Sciences, Plot 56 University Pool Road, Kampala, Uganda -

-

- E:{' '} - - info@airqo.net - -

-
-
- - {/* Inquiry Options Section */} - - {[ - { - icon: , - question: 'I have a question about', - detail: 'Air Quality Tools', - }, - { - icon: , - question: 'I have a question about', - detail: 'Air Quality Data', - }, - { - icon: , - question: 'I have some', - detail: 'feedback', - }, - { - icon: , - question: 'I have a', - detail: 'general inquiry', - }, - ].map((item, index) => ( - - ))} - -
- ); -}; - -export default ContactPage; +'use client'; +import { motion } from 'framer-motion'; +import { useRouter } from 'next/navigation'; +import React from 'react'; +import { FiDatabase, FiMessageCircle, FiStar, FiTablet } from 'react-icons/fi'; + +const ContactPage: React.FC = () => { + const router = useRouter(); + + const handleButtonClick = (detail: string) => { + router.push( + `/contact/form?category=${detail.toLowerCase().replace(/ /g, '-')}`, + ); + }; + + // Animation variants for the container + const containerVariants = { + hidden: { opacity: 0, y: 50 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.6, + ease: 'easeOut', + staggerChildren: 0.2, + }, + }, + }; + + // Animation variants for each item + const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.5, ease: 'easeOut' }, + }, + }; + + return ( +
+ {/* Contact Information Section */} +
+ +

Get in touch

+

Makerere University

+

+ Software Systems Centre, Block B, Level 3, College of Computing and + Information Sciences, Plot 56 University Pool Road, Kampala, Uganda +

+

+ E:{' '} + + info@airqo.net + +

+
+
+ + {/* Inquiry Options Section */} + + {[ + { + icon: , + question: 'I have a question about', + detail: 'Air Quality Tools', + }, + { + icon: , + question: 'I have a question about', + detail: 'Air Quality Data', + }, + { + icon: , + question: 'I have some', + detail: 'feedback', + }, + { + icon: , + question: 'I have a', + detail: 'general inquiry', + }, + ].map((item, index) => ( + + ))} + +
+ ); +}; + +export default ContactPage; diff --git a/website2/src/app/contact/form/FormPage.tsx b/website2/src/app/[locale]/contact/form/FormPage.tsx similarity index 96% rename from website2/src/app/contact/form/FormPage.tsx rename to website2/src/app/[locale]/contact/form/FormPage.tsx index 4029a5cdfd..75e1878bab 100644 --- a/website2/src/app/contact/form/FormPage.tsx +++ b/website2/src/app/[locale]/contact/form/FormPage.tsx @@ -1,268 +1,268 @@ -'use client'; - -import { motion } from 'framer-motion'; -import { useRouter, useSearchParams } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; -import { FiArrowLeft } from 'react-icons/fi'; - -import { CustomButton } from '@/components/ui'; -import { postContactUs } from '@/services/externalService'; - -const FormPage: React.FC = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - const [category, setCategory] = useState(null); - - // State for form data, loading, error, and success messages - const [formData, setFormData] = useState({ - fullName: '', - email: '', - message: '', - }); - const [loading, setLoading] = useState(false); - const [success, setSuccess] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - const categoryFromUrl = searchParams.get('category'); - setCategory(categoryFromUrl); - }, [searchParams]); - - // Handle form input changes - const handleInputChange = ( - e: React.ChangeEvent, - ) => { - const { name, value } = e.target; - setFormData((prev) => ({ - ...prev, - [name]: value, - })); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setLoading(true); - setError(null); - - const body = { - ...formData, - category: category || 'general', - }; - - try { - const response = await postContactUs(body); - - if (response.success) { - setSuccess(true); - setFormData({ - fullName: '', - email: '', - message: '', - }); - } else { - throw new Error('Failed to submit form'); - } - } catch (err) { - setError('Oops! Something went wrong. Please try again.'); - console.error(err); - } finally { - setLoading(false); - } - }; - - // Define motion variants for the form container - const formVariants = { - hidden: { opacity: 0, scale: 0.95 }, - visible: { - opacity: 1, - scale: 1, - transition: { duration: 0.5, ease: 'easeOut' }, - }, - }; - - if (success) { - return ( -
- -

Success!

-

- Your message has been sent successfully. -

- router.push('/')} - className="mt-4 bg-blue-600 text-white px-6 py-4 hover:bg-blue-700 transition-colors" - > - Go to Homepage - -
-
- ); - } - - return ( -
- {/* Contact Information Section */} -
-
-

Get in touch

-

Makerere University

-

- Software Systems Centre, Block B, Level 3, College of Computing and - Information Sciences, Plot 56 University Pool Road, Kampala, Uganda -

-

- E:{' '} - - info@airqo.net - -

-
-
- - {/* Form Section with Framer Motion */} -
- {/* Form */} - - {/* Back Button */} - - -
- {/* Full Name */} -
- - -
- - {/* Email Address */} -
- - -
- - {/* Message */} -
- -