diff --git a/k8s/platform/values-prod.yaml b/k8s/platform/values-prod.yaml index d70a342f35..6043bc5b08 100644 --- a/k8s/platform/values-prod.yaml +++ b/k8s/platform/values-prod.yaml @@ -2,7 +2,7 @@ replicaCount: 1 image: repository: eu.gcr.io/airqo-250220/airqo-next-platform pullPolicy: Always - tag: prod-16e65200-1739133930 + tag: prod-6a098ecd-1740727843 imagePullSecrets: [] nameOverride: '' fullnameOverride: '' diff --git a/k8s/platform/values-stage.yaml b/k8s/platform/values-stage.yaml index 435920b26e..be349674aa 100644 --- a/k8s/platform/values-stage.yaml +++ b/k8s/platform/values-stage.yaml @@ -2,7 +2,7 @@ replicaCount: 1 image: repository: eu.gcr.io/airqo-250220/airqo-stage-next-platform pullPolicy: Always - tag: stage-963fc29b-1740488187 + tag: stage-93a63a9e-1740727732 imagePullSecrets: [] nameOverride: '' fullnameOverride: '' diff --git a/src/platform/src/core/utils/protectedRoute.js b/src/platform/src/core/utils/protectedRoute.js index cc2bbf2469..7df4e81298 100644 --- a/src/platform/src/core/utils/protectedRoute.js +++ b/src/platform/src/core/utils/protectedRoute.js @@ -2,87 +2,17 @@ import React, { useEffect } from 'react'; import { useRouter } from 'next/router'; import { useSelector, useDispatch } from 'react-redux'; import LogoutUser from '@/core/utils/LogoutUser'; -import Cookies from 'js-cookie'; -import jwt_decode from 'jwt-decode'; -import { - setUserInfo, - setSuccess, -} from '@/lib/store/services/account/LoginSlice'; -import { getIndividualUserPreferences } from '@/lib/store/services/account/UserDefaultsSlice'; -import { getUserDetails } from '@/core/apis/Account'; -import Spinner from '../../common/components/Spinner'; - -const MAX_RETRIES = 3; -const RETRY_DELAY = 1000; export default function withAuth(Component) { return function WithAuthComponent(props) { const dispatch = useDispatch(); const router = useRouter(); const userCredentials = useSelector((state) => state.login); - const [isRedirecting, setIsRedirecting] = React.useState( - router.query.success === 'google', - ); - - const retryWithDelay = async (fn, retries = MAX_RETRIES) => { - try { - return await fn(); - } catch (error) { - if (retries > 0 && error.response?.status === 429) { - await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); - return retryWithDelay(fn, retries - 1); - } - throw error; - } - }; - - const setupUserSession = async (user) => { - if (!user.groups[0]?.grp_title) { - throw new Error( - 'Server error. Contact support to add you to the AirQo Organisation', - ); - } - - localStorage.setItem('loggedUser', JSON.stringify(user)); - - const preferencesResponse = await retryWithDelay(() => - dispatch(getIndividualUserPreferences({ identifier: user._id })), - ); - if (preferencesResponse.payload.success) { - const preferences = preferencesResponse.payload.preferences; - const activeGroup = preferences[0]?.group_id - ? user.groups.find((group) => group._id === preferences[0].group_id) - : user.groups.find((group) => group.grp_title === 'airqo'); - localStorage.setItem('activeGroup', JSON.stringify(activeGroup)); - } - - dispatch(setUserInfo(user)); - dispatch(setSuccess(true)); - }; useEffect(() => { if (typeof window !== 'undefined') { - // Handle Google redirect first - if (router.query.success === 'google') { - const token = Cookies.get('access_token'); - if (token) { - localStorage.setItem('token', token); - const decoded = jwt_decode(token); - retryWithDelay(() => getUserDetails(decoded._id, token)) - .then((response) => setupUserSession(response.users[0])) - .catch((error) => { - console.error('Google auth error:', error); - setIsRedirecting(false); - router.push('/account/login'); - }); - } else { - setIsRedirecting(false); - router.push('/account/login'); - } - return; // Exit early to prevent further checks until redirect is resolved - } - const storedUserGroup = localStorage.getItem('activeGroup'); + if (!userCredentials.success) { router.push('/account/login'); } @@ -91,12 +21,7 @@ export default function withAuth(Component) { LogoutUser(dispatch, router); } } - }, [userCredentials, dispatch, router, retryWithDelay, isRedirecting]); - - // Block rendering until redirect is handled - if (isRedirecting) { - return ; - } + }, [userCredentials, dispatch, router]); // Render the component if the user is authenticated return userCredentials.success ? : null; diff --git a/src/platform/src/pages/account/creation/index.jsx b/src/platform/src/pages/account/creation/index.jsx index 94a428bfc2..51c184a39e 100644 --- a/src/platform/src/pages/account/creation/index.jsx +++ b/src/platform/src/pages/account/creation/index.jsx @@ -4,18 +4,26 @@ import GoogleLogo from '@/icons/Common/google_logo.svg'; import { getGoogleAuthDetails } from '@/core/apis/Account'; import CheckComponent from '@/components/Account/CheckComponent'; +const FORM_URL = 'https://forms.gle/VX5p2s65n8U51iBc8'; + const userRoles = [ { title: 'Individual', subText: 'Empower yourself with real-time Air Pollution Location Data for research and personal use. Stay informed, stay healthy. Join the clean air revolution today.', disabled: false, + // Internal route for individuals. + route: (router) => router.push(`/account/creation/individual/register`), }, { title: 'Organisation', subText: 'Beyond data, gain access to network management tools. Drive meaningful change, one location at a time. Shape a cleaner future for all.', - disabled: true, + disabled: false, + // External route for organisations. + route: () => { + window.location.href = FORM_URL; + }, }, ]; @@ -23,24 +31,27 @@ const UserDesignation = () => { const [clickedRole, setClickedRole] = useState(''); const router = useRouter(); - const routeToCreation = () => { - if (clickedRole) { - router.push(`/account/creation/${clickedRole.toLowerCase()}/register`); - } + const handleRoleClick = (roleTitle, disabled) => { + if (disabled) return; + setClickedRole((prevRole) => (prevRole === roleTitle ? '' : roleTitle)); }; - const handleRoleClick = (roleTitle) => { - setClickedRole((prevRole) => (prevRole === roleTitle ? '' : roleTitle)); + const routeToCreation = () => { + if (!clickedRole) return; + const selectedRole = userRoles.find((role) => role.title === clickedRole); + if (selectedRole && selectedRole.route) { + selectedRole.route(router); + } }; return (
-
+

How are you planning to use AirQo Analytics?

- We'll streamline your setup experience accordingly + We'll streamline your setup experience accordingly

@@ -48,24 +59,18 @@ const UserDesignation = () => {
{ - if (role.disabled) { - return; - } - handleRoleClick(role.title); - }} + role="button" + tabIndex={0} + onClick={() => handleRoleClick(role.title, role.disabled)} onKeyUp={(e) => { if (e.key === 'Enter' || e.key === ' ') { - if (role.disabled) { - return; - } - handleRoleClick(role.title); + handleRoleClick(role.title, role.disabled); } }} > { onClick={routeToCreation} className="mt-6 w-[262px] flex justify-center items-center px-4 py-2 bg-blue-600 text-white rounded-[12px]" > - Continue + {clickedRole === 'Organisation' ? 'Get started' : 'Continue'} )}
@@ -87,31 +92,27 @@ const UserDesignation = () => { ); }; -const GoogleAccountCreation = () => { - return ( -
-
- - Or - -
-
- -
+const GoogleAccountCreation = () => ( +
+
+ + Or +
- ); -}; +
+ +
+
+); export default UserDesignation; diff --git a/src/platform/src/pages/account/login/index.jsx b/src/platform/src/pages/account/login/index.jsx index edbf1b764d..4a28bdac20 100644 --- a/src/platform/src/pages/account/login/index.jsx +++ b/src/platform/src/pages/account/login/index.jsx @@ -18,7 +18,6 @@ import { } from '@/lib/store/services/account/LoginSlice'; import { getIndividualUserPreferences } from '@/lib/store/services/account/UserDefaultsSlice'; import { postUserLoginDetails, getUserDetails } from '@/core/apis/Account'; -import { GOOGLE_AUTH_URL } from '@/core/urls/authentication'; const MAX_RETRIES = 3; const RETRY_DELAY = 1000; @@ -111,15 +110,6 @@ const UserLogin = () => { dispatch(setUserData({ key, value })); }; - const handleGoogleLogin = async () => { - try { - // Redirect to Google auth URL - window.location.href = GOOGLE_AUTH_URL; - } catch (error) { - console.error('Login error:', error); - } - }; - return ( {
-
- -
diff --git a/src/website2/next.config.mjs b/src/website2/next.config.mjs index 60418cc7cc..8a23ebaa96 100644 --- a/src/website2/next.config.mjs +++ b/src/website2/next.config.mjs @@ -46,6 +46,21 @@ const nextConfig = { destination: '/clean-air-forum/about', permanent: true, }, + { + source: '/clean-air-network', + destination: '/clean-air-network/about', + permanent: true, + }, + { + source: '/clean-air/about', + destination: '/clean-air-network/about', + permanent: true, + }, + { + source: '/clean-air', + destination: '/clean-air-network/about', + permanent: true, + }, ]; }, }; diff --git a/src/website2/public/Logo.png b/src/website2/public/Logo.png new file mode 100644 index 0000000000..bf64b5cbd5 Binary files /dev/null and b/src/website2/public/Logo.png differ diff --git a/src/website2/public/apple-icon.png b/src/website2/public/apple-icon.png new file mode 100644 index 0000000000..6af6fa1c8e Binary files /dev/null and b/src/website2/public/apple-icon.png differ diff --git a/src/website2/public/assets/icons/Icon1.tsx b/src/website2/public/assets/icons/Icon1.tsx index 76615bec02..be291f7323 100644 --- a/src/website2/public/assets/icons/Icon1.tsx +++ b/src/website2/public/assets/icons/Icon1.tsx @@ -1,4 +1,4 @@ -// components/icons/Icon1.tsx + import React from 'react'; interface IconProps { diff --git a/src/website2/public/assets/icons/Icon2.tsx b/src/website2/public/assets/icons/Icon2.tsx index dbd81a690d..3e6d88d384 100644 --- a/src/website2/public/assets/icons/Icon2.tsx +++ b/src/website2/public/assets/icons/Icon2.tsx @@ -1,4 +1,4 @@ -// components/icons/Icon2.tsx + import React from 'react'; interface IconProps { diff --git a/src/website2/public/assets/icons/Icon3.tsx b/src/website2/public/assets/icons/Icon3.tsx index 5812824619..71fa508107 100644 --- a/src/website2/public/assets/icons/Icon3.tsx +++ b/src/website2/public/assets/icons/Icon3.tsx @@ -1,4 +1,4 @@ -// components/icons/Icon3.tsx + import React from 'react'; interface IconProps { diff --git a/src/website2/public/assets/icons/Icon4.tsx b/src/website2/public/assets/icons/Icon4.tsx index 40c3fa8ba1..46f132ef6b 100644 --- a/src/website2/public/assets/icons/Icon4.tsx +++ b/src/website2/public/assets/icons/Icon4.tsx @@ -1,4 +1,4 @@ -// components/icons/Icon4.tsx + import React from 'react'; interface IconProps { diff --git a/src/website2/public/assets/icons/Icon5.tsx b/src/website2/public/assets/icons/Icon5.tsx index 7da0d0487b..5483e5940f 100644 --- a/src/website2/public/assets/icons/Icon5.tsx +++ b/src/website2/public/assets/icons/Icon5.tsx @@ -1,4 +1,4 @@ -// components/icons/Icon5.tsx + import React from 'react'; interface IconProps { diff --git a/src/website2/public/assets/icons/Icon6.tsx b/src/website2/public/assets/icons/Icon6.tsx index 3b62e3f341..26911b5ca2 100644 --- a/src/website2/public/assets/icons/Icon6.tsx +++ b/src/website2/public/assets/icons/Icon6.tsx @@ -1,4 +1,4 @@ -// components/icons/Icon6.tsx + import React from 'react'; interface IconProps { diff --git a/src/website2/public/assets/icons/Icon7.tsx b/src/website2/public/assets/icons/Icon7.tsx index 38af9fcba9..a8ebbde45c 100644 --- a/src/website2/public/assets/icons/Icon7.tsx +++ b/src/website2/public/assets/icons/Icon7.tsx @@ -1,4 +1,4 @@ -// components/icons/Icon6.tsx + import React from 'react'; interface IconProps { diff --git a/src/website2/public/assets/icons/Icon8.tsx b/src/website2/public/assets/icons/Icon8.tsx index 272fbb43a8..e7016fc1e5 100644 --- a/src/website2/public/assets/icons/Icon8.tsx +++ b/src/website2/public/assets/icons/Icon8.tsx @@ -1,4 +1,4 @@ -// components/icons/Icon6.tsx + import React from 'react'; interface IconProps { diff --git a/src/website2/public/favicon.ico b/src/website2/public/favicon.ico new file mode 100644 index 0000000000..80f940cae3 Binary files /dev/null and b/src/website2/public/favicon.ico differ diff --git a/src/website2/public/icon.png b/src/website2/public/icon.png new file mode 100644 index 0000000000..fd9cd302d8 Binary files /dev/null and b/src/website2/public/icon.png differ diff --git a/src/website2/public/icon.svg b/src/website2/public/icon.svg new file mode 100644 index 0000000000..e925bedccd --- /dev/null +++ b/src/website2/public/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/website2/public/manifest.json b/src/website2/public/manifest.json new file mode 100644 index 0000000000..859342a095 --- /dev/null +++ b/src/website2/public/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "AirQo Website", + "short_name": "AirQo", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/src/website2/public/web-app-manifest-192x192.png b/src/website2/public/web-app-manifest-192x192.png new file mode 100644 index 0000000000..1f574466b5 Binary files /dev/null and b/src/website2/public/web-app-manifest-192x192.png differ diff --git a/src/website2/public/web-app-manifest-512x512.png b/src/website2/public/web-app-manifest-512x512.png new file mode 100644 index 0000000000..9a57e19709 Binary files /dev/null and b/src/website2/public/web-app-manifest-512x512.png differ diff --git a/src/website2/src/app/clean-air-forum/about/page.tsx b/src/website2/src/app/clean-air-forum/about/page.tsx index 20109a061c..edb4bd14f0 100644 --- a/src/website2/src/app/clean-air-forum/about/page.tsx +++ b/src/website2/src/app/clean-air-forum/about/page.tsx @@ -1,8 +1,14 @@ -import React from 'react'; +import { Metadata } from 'next'; -import AboutPage from '@/views/Forum/AboutPage'; +import AboutPage from '@/views/cleanairforum/about/AboutPage'; -const page = () => { +export const metadata: Metadata = { + title: 'About Clean Air Forum | AirQo', + description: + 'Discover the Clean Air Forum – learn about our mission, vision, and how we foster collaboration to advance clean air solutions and improve air quality.', +}; + +const Page = () => { return (
@@ -10,4 +16,4 @@ const page = () => { ); }; -export default page; +export default Page; diff --git a/src/website2/src/app/clean-air-forum/glossary/page.tsx b/src/website2/src/app/clean-air-forum/glossary/page.tsx index d099dd930e..6ee00f95fc 100644 --- a/src/website2/src/app/clean-air-forum/glossary/page.tsx +++ b/src/website2/src/app/clean-air-forum/glossary/page.tsx @@ -1,115 +1,15 @@ -'use client'; +import { Metadata } from 'next'; -import DOMPurify from 'dompurify'; -import Link from 'next/link'; -import React from 'react'; +import GlossaryPage from '@/views/cleanairforum/glossary/GlossaryPage'; -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 */} -
-
- - )} +export const metadata: Metadata = { + title: 'Glossary | Clean Air Forum', + description: + 'Explore our glossary of key terms and pollutant types used at Clean Air Forum, providing clear definitions to help you better understand air quality management.', +}; - {/* Additional Glossary Sections (if any) */} - {glossarySections && glossarySections.length > 0 && ( - <> - {glossarySections.map((section: any) => ( - - ))} - - )} -
- ); +const Page = () => { + return ; }; -export default GlossaryPage; +export default Page; diff --git a/src/website2/src/app/clean-air-forum/layout.tsx b/src/website2/src/app/clean-air-forum/layout.tsx index 260174194f..6b1604d5f7 100644 --- a/src/website2/src/app/clean-air-forum/layout.tsx +++ b/src/website2/src/app/clean-air-forum/layout.tsx @@ -1,4 +1,3 @@ -// components/layouts/CleanAirLayout.tsx 'use client'; import { useSearchParams } from 'next/navigation'; @@ -12,7 +11,8 @@ 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'; + +import BannerSection from '../../views/cleanairforum/BannerSection'; type CleanAirLayoutProps = { children: ReactNode; diff --git a/src/website2/src/app/clean-air-forum/logistics/page.tsx b/src/website2/src/app/clean-air-forum/logistics/page.tsx index abbadd07df..a358f9dc32 100644 --- a/src/website2/src/app/clean-air-forum/logistics/page.tsx +++ b/src/website2/src/app/clean-air-forum/logistics/page.tsx @@ -1,96 +1,15 @@ -'use client'; +import { Metadata } from 'next'; -import DOMPurify from 'dompurify'; -import React from 'react'; +import LogisticsPage from '@/views/cleanairforum/logistics/LogisticsPage'; -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 -

-
-
-
-
- - )} +export const metadata: Metadata = { + title: 'Logistics | Clean Air Forum', + description: + 'Get all the essential logistics information for attending the Clean Air Forum, including travel, accommodation, event schedules, and practical tips for participants.', +}; - {/* Render additional Logistics Sections, if any */} - {logisticsSections && logisticsSections.length > 0 && ( - <> - {logisticsSections.map((section: any) => ( - - ))} - - )} -
- ); +const Page = () => { + return ; }; -export default LogisticsPage; +export default Page; diff --git a/src/website2/src/app/clean-air-forum/partners/page.tsx b/src/website2/src/app/clean-air-forum/partners/page.tsx index ab8ecb1556..feffb94e35 100644 --- a/src/website2/src/app/clean-air-forum/partners/page.tsx +++ b/src/website2/src/app/clean-air-forum/partners/page.tsx @@ -1,148 +1,15 @@ -'use client'; +import { Metadata } from 'next'; -import DOMPurify from 'dompurify'; -import React from 'react'; +import PartnersPage from '@/views/cleanairforum/partners/PartnersPage'; -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

-
- -
- - )} +export const metadata: Metadata = { + title: 'Partners | Clean Air Forum | AirQo', + description: + 'Discover the partners collaborating with Clean Air Forum to advance clean air solutions and improve air quality through innovation and community engagement.', +}; - {fundingPartners && fundingPartners.length > 0 && ( - <> - -
-
-

- Funding Partners and Sponsors -

-
- -
- - )} -
- ); +const Page = () => { + return ; }; -export default PartnersPage; +export default Page; diff --git a/src/website2/src/app/clean-air-forum/program-committee/page.tsx b/src/website2/src/app/clean-air-forum/program-committee/page.tsx index ab3f7d2b3d..b594c3c863 100644 --- a/src/website2/src/app/clean-air-forum/program-committee/page.tsx +++ b/src/website2/src/app/clean-air-forum/program-committee/page.tsx @@ -1,121 +1,15 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -'use client'; +import { Metadata } from 'next'; -import DOMPurify from 'dompurify'; -import React, { useMemo, useState } from 'react'; +import CommitteePage from '@/views/cleanairforum/program-committee/CommitteePage'; -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) => ( - - ))} -
+export const metadata: Metadata = { + title: 'Program Committee | Clean Air Forum | AirQo', + description: + 'Meet the expert program committee of Clean Air Forum. Learn about the thought leaders and industry experts driving innovative strategies for better air quality.', +}; - {/* Pagination Component */} - {totalPages > 1 && ( -
- -
- )} -
- ); +const Page = () => { + return ; }; -export default CommitteePage; +export default Page; diff --git a/src/website2/src/app/clean-air-forum/resources/page.tsx b/src/website2/src/app/clean-air-forum/resources/page.tsx index a107048955..7e848f67e6 100644 --- a/src/website2/src/app/clean-air-forum/resources/page.tsx +++ b/src/website2/src/app/clean-air-forum/resources/page.tsx @@ -1,148 +1,15 @@ -'use client'; +import { Metadata } from 'next'; -import React, { useState } from 'react'; -import { FaChevronDown, FaChevronUp, FaFilePdf } from 'react-icons/fa'; +import ResourcesPage from '@/views/cleanairforum/resources/ResourcesPage'; -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)} - -
-
-
- ))} -
- )} -
- ); +export const metadata: Metadata = { + title: 'Resources | Clean Air Forum | AirQo', + description: + 'Access comprehensive resources, presentations, and documents from Clean Air Forum to stay informed about the latest trends and innovations in air quality management.', }; -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) - } - /> - ), - )} - -
- ), - )} -
- ); +const Page = () => { + return ; }; -export default ResourcesPage; +export default Page; diff --git a/src/website2/src/app/clean-air-forum/sessions/page.tsx b/src/website2/src/app/clean-air-forum/sessions/page.tsx index 010ab091cf..ecb0a22762 100644 --- a/src/website2/src/app/clean-air-forum/sessions/page.tsx +++ b/src/website2/src/app/clean-air-forum/sessions/page.tsx @@ -1,158 +1,15 @@ -'use client'; +import { Metadata } from 'next'; -import { format } from 'date-fns'; -import DOMPurify from 'dompurify'; -import React, { useState } from 'react'; -import { FaChevronDown, FaChevronUp } from 'react-icons/fa'; +import ProgramsPage from '@/views/cleanairforum/sessions-programs/ProgramsPage'; -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}

-
-
-
-
- ))} -
- )} -
- ); +export const metadata: Metadata = { + title: 'Sessions & Programs | Clean Air Forum | AirQo', + description: + 'Explore the schedule and detailed program information for Clean Air Forum. Find out about the sessions, topics, and speakers shaping the future of air quality management.', }; -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

-
-
-
-
- )} -
- ); +const Page = () => { + return ; }; -export default ProgramsPage; +export default Page; diff --git a/src/website2/src/app/clean-air-forum/speakers/page.tsx b/src/website2/src/app/clean-air-forum/speakers/page.tsx index af98135d6f..155bca24e6 100644 --- a/src/website2/src/app/clean-air-forum/speakers/page.tsx +++ b/src/website2/src/app/clean-air-forum/speakers/page.tsx @@ -1,147 +1,15 @@ -'use client'; +import { Metadata } from 'next'; -import DOMPurify from 'dompurify'; -import React, { useState } from 'react'; +import SpeakersPage from '@/views/cleanairforum/speakers/SpeakersPage'; -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 && ( -
- -
- )} +export const metadata: Metadata = { + title: 'Speakers | Clean Air Forum | AirQo', + description: + 'Meet the distinguished speakers at Clean Air Forum. Learn from industry leaders and experts who are driving change in air quality and environmental management.', +}; - {/* Extra Speakers Sections */} - {speakersExtraSections && speakersExtraSections.length > 0 && ( - <> - {speakersExtraSections.map((section: any) => ( - - ))} - - )} -
- ); +const Page = () => { + return ; }; -export default SpeakersPage; +export default Page; diff --git a/src/website2/src/app/clean-air-forum/sponsorships/page.tsx b/src/website2/src/app/clean-air-forum/sponsorships/page.tsx index 28e2a89534..7e3e96f2eb 100644 --- a/src/website2/src/app/clean-air-forum/sponsorships/page.tsx +++ b/src/website2/src/app/clean-air-forum/sponsorships/page.tsx @@ -1,82 +1,15 @@ -'use client'; +import { Metadata } from 'next'; -import DOMPurify from 'dompurify'; -import React from 'react'; +import SponsorshipPage from '@/views/cleanairforum/sponsorship/SponsorshipPage'; -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) => ( - - ))} - - )} +export const metadata: Metadata = { + title: 'Sponsorship | Clean Air Forum | AirQo', + description: + 'Discover sponsorship opportunities for Clean Air Forum. Learn about tailored sponsorship packages designed to support innovation and community engagement in air quality management.', +}; - {sponsorPartner && sponsorPartner.length > 0 && ( - <> - -
-
-
-

- Sponsors -

-
- -
-
- - )} -
- ); +const Page = () => { + return ; }; -export default SponsorshipPage; +export default Page; diff --git a/src/website2/src/app/clean-air-network/about/page.tsx b/src/website2/src/app/clean-air-network/about/page.tsx new file mode 100644 index 0000000000..b97864b717 --- /dev/null +++ b/src/website2/src/app/clean-air-network/about/page.tsx @@ -0,0 +1,19 @@ +import { Metadata } from 'next'; + +import CleanAirPage from '@/views/cleanAirNetwork/about/CleanAirPage'; + +export const metadata: Metadata = { + title: 'About Clean Air Network | AirQo', + description: + 'Discover Clean Air Network – our mission, vision, and the collaborative efforts we undertake to drive innovation and improve air quality through community and stakeholder engagement.', +}; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export default Page; diff --git a/src/website2/src/app/clean-air-network/events/[id]/page.tsx b/src/website2/src/app/clean-air-network/events/[id]/page.tsx index 6b46e523ca..a30542cae5 100644 --- a/src/website2/src/app/clean-air-network/events/[id]/page.tsx +++ b/src/website2/src/app/clean-air-network/events/[id]/page.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import SingleEvent from '@/views/cleanairforum/events/SingleEvent'; +import SingleEvent from '@/views/cleanAirNetwork/events/SingleEvent'; const page = ({ params }: { params: any }) => { return ( diff --git a/src/website2/src/app/clean-air-network/events/page.tsx b/src/website2/src/app/clean-air-network/events/page.tsx index 1033d25b12..e2b83ddc5e 100644 --- a/src/website2/src/app/clean-air-network/events/page.tsx +++ b/src/website2/src/app/clean-air-network/events/page.tsx @@ -1,6 +1,14 @@ -import EventsPage from '@/views/cleanairforum/events/EventsPage'; +import { Metadata } from 'next'; -const page = () => { +import EventsPage from '@/views/cleanAirNetwork/events/EventsPage'; + +export const metadata: Metadata = { + title: 'Events | Clean Air Network | AirQo', + description: + 'Explore upcoming and past events hosted by Clean Air Network. Stay informed about conferences, webinars, and networking opportunities designed to advance clean air initiatives.', +}; + +const Page = () => { return (
@@ -8,4 +16,4 @@ const page = () => { ); }; -export default page; +export default Page; diff --git a/src/website2/src/app/clean-air-network/membership/page.tsx b/src/website2/src/app/clean-air-network/membership/page.tsx index ee1803b08d..f57f069738 100644 --- a/src/website2/src/app/clean-air-network/membership/page.tsx +++ b/src/website2/src/app/clean-air-network/membership/page.tsx @@ -1,6 +1,14 @@ -import MemberPage from '@/views/cleanairforum/membership/MemberPage'; +import { Metadata } from 'next'; -const page = () => { +import MemberPage from '@/views/cleanAirNetwork/membership/MemberPage'; + +export const metadata: Metadata = { + title: 'Membership | Clean Air Network | AirQo', + description: + 'Join Clean Air Network – connect with professionals dedicated to advancing air quality. Learn about membership benefits, exclusive resources, and opportunities for collaboration.', +}; + +const Page = () => { return (
@@ -8,4 +16,4 @@ const page = () => { ); }; -export default page; +export default Page; diff --git a/src/website2/src/app/clean-air-network/page.tsx b/src/website2/src/app/clean-air-network/page.tsx deleted file mode 100644 index b7a801b61d..0000000000 --- a/src/website2/src/app/clean-air-network/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; - -import CleanAirPage from './CleanAirPage'; - -const page = () => { - return ( -
- -
- ); -}; - -export default page; diff --git a/src/website2/src/app/clean-air-network/resources/page.tsx b/src/website2/src/app/clean-air-network/resources/page.tsx index 805d6b0e40..985d5abf52 100644 --- a/src/website2/src/app/clean-air-network/resources/page.tsx +++ b/src/website2/src/app/clean-air-network/resources/page.tsx @@ -1,6 +1,14 @@ -import ResourcePage from '@/views/cleanairforum/resources/ResourcePage'; +import { Metadata } from 'next'; -const page = () => { +import ResourcePage from '@/views/cleanAirNetwork/resources/ResourcePage'; + +export const metadata: Metadata = { + title: 'Resources | Clean Air Network | AirQo', + description: + 'Access a wide range of resources and insights on air quality management from Clean Air Network. Explore reports, case studies, and research to stay ahead in clean air initiatives.', +}; + +const Page = () => { return (
@@ -8,4 +16,4 @@ const page = () => { ); }; -export default page; +export default Page; diff --git a/src/website2/src/app/favicon.ico b/src/website2/src/app/favicon.ico index 2e026677d8..80f940cae3 100644 Binary files a/src/website2/src/app/favicon.ico and b/src/website2/src/app/favicon.ico differ diff --git a/src/website2/src/app/layout.tsx b/src/website2/src/app/layout.tsx index 2f8b995d6f..a6061ec024 100644 --- a/src/website2/src/app/layout.tsx +++ b/src/website2/src/app/layout.tsx @@ -1,7 +1,7 @@ import './globals.css'; -import dynamic from 'next/dynamic'; import localFont from 'next/font/local'; +import Script from 'next/script'; import { ReactNode, Suspense } from 'react'; import EngagementDialog from '@/components/dialogs/EngagementDialog'; @@ -12,11 +12,6 @@ import { checkMaintenance } from '@/lib/maintenance'; import MaintenancePage from './MaintenancePage'; -// Load the GA component dynamically, disabling SSR so that it runs only on the client. -const GoogleAnalytics = dynamic(() => import('@/components/GoogleAnalytics'), { - ssr: false, -}); - const interFont = localFont({ src: [ { @@ -38,19 +33,128 @@ export default async function RootLayout({ }: { children: ReactNode; }) { + const GA_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || 'G-79ZVCLEDSG'; + const siteUrl = 'https://airqo.net/'; + const title = 'AirQo | Bridging the Air Quality Data Gap in Africa'; + const description = + 'AirQo is transforming air quality management in Africa by providing low-cost sensors, real-time data, and actionable insights to help communities and organizations improve air quality.'; + + const keywords = [ + 'AirQo', + 'air quality monitoring', + 'air pollution', + 'PM1', + 'PM2.5', + 'PM10', + 'NO2', + 'SO2', + 'CO', + 'O3', + 'air quality index', + 'AQI', + 'real-time air quality data', + 'low-cost air sensors', + 'urban air pollution', + 'environmental monitoring', + 'climate change', + 'air quality management', + 'clean air solutions', + 'air quality in Africa', + 'environmental health', + 'ambient air monitoring', + 'particulate matter', + 'air quality forecasting', + 'air quality analytics', + 'pollution mitigation', + 'environmental data', + 'sustainable cities', + 'public health', + 'respiratory health', + 'environmental policy', + 'air quality research', + 'air quality standards', + 'air quality compliance', + 'air pollution control', + 'air quality education', + ].join(', '); + const maintenance = await checkMaintenance(); - const GA_MEASUREMENT_ID = - process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || 'G-79ZVCLEDSG'; return ( + + {/* Primary SEO */} + {title} + + + + + + + {/* Real Favicon Generator / Favicon + App Icons */} + + + + + + + {/* Windows / MS Tiles */} + + + + {/* Manifest */} + + + + {/* Open Graph / Facebook */} + + + + + + + + + {/* Twitter */} + + + + + + + {/* Canonical URL */} + + + {/* GA snippet must appear in for Search Console verification */} + + }> - {/* Initialize & Track Google Analytics only on the client */} - - {maintenance.isActive ? ( ) : ( diff --git a/src/website2/src/components/dialogs/EngagementDialog.tsx b/src/website2/src/components/dialogs/EngagementDialog.tsx index b058037e42..1bfd87d414 100644 --- a/src/website2/src/components/dialogs/EngagementDialog.tsx +++ b/src/website2/src/components/dialogs/EngagementDialog.tsx @@ -10,6 +10,7 @@ import { useDispatch, useSelector } from '@/hooks'; import { postContactUs } from '@/services/externalService'; import { closeModal } from '@/store/slices/modalSlice'; +import { trackEvent } from '../GoogleAnalytics'; import { CustomButton } from '../ui'; interface EngagementOption { @@ -172,6 +173,17 @@ const EngagementDialog = () => { const res = await postContactUs(requestBody); if (res.success) { setSubmissionSuccess(true); + + // Fire the tracking event only on successful submission. + trackEvent({ + action: 'submit_form', + category: 'engagement_dialog', + label: selectedCategory + ? `engagement_dialog_submit_form_success_${selectedCategory}` + : 'engagement_dialog_submit_form_success', + }); + + // Reset form data after successful submission setFormData({ firstName: '', lastName: '', diff --git a/src/website2/src/components/layouts/Footer.tsx b/src/website2/src/components/layouts/Footer.tsx index 24c76d3f8e..d70f13fd4f 100644 --- a/src/website2/src/components/layouts/Footer.tsx +++ b/src/website2/src/components/layouts/Footer.tsx @@ -1,12 +1,8 @@ import Image from 'next/image'; import Link from 'next/link'; import React from 'react'; -import { - FaFacebookF, - FaLinkedinIn, - FaTwitter, - FaYoutube, -} from 'react-icons/fa'; +import { FaFacebookF, FaLinkedinIn, FaYoutube } from 'react-icons/fa'; +import { FaXTwitter } from 'react-icons/fa6'; import mainConfig from '@/configs/mainConfigs'; @@ -33,9 +29,9 @@ const Footer = () => { width={70} height={60} /> -

+

Clean air for all
African Cities. -

+

{ aria-label="Twitter" className="text-blue-600 bg-blue-50 rounded-full p-2 hover:bg-blue-200 transition-all" > - +
diff --git a/src/website2/src/components/layouts/Navbar.tsx b/src/website2/src/components/layouts/Navbar.tsx index 8d266dad70..fbb9938c36 100644 --- a/src/website2/src/components/layouts/Navbar.tsx +++ b/src/website2/src/components/layouts/Navbar.tsx @@ -20,7 +20,7 @@ import { import mainConfig from '@/configs/mainConfigs'; import { useDispatch } from '@/hooks'; import { openModal } from '@/store/slices/modalSlice'; -import TabNavigation from '@/views/cleanairforum/TabNavigation'; +import TabNavigation from '@/views/cleanAirNetwork/TabNavigation'; import { trackEvent } from '../GoogleAnalytics'; import NotificationBanner from './NotificationBanner'; @@ -225,13 +225,27 @@ const Navbar: React.FC = () => { ))} dispatch(openModal())} + onClick={() => { + trackEvent({ + action: 'button_click', + category: 'engagement', + label: 'get_involved', + }); + dispatch(openModal()); + }} className="text-blue-600 bg-blue-50 transition rounded-none" > Get involved router.push('/explore-data')} + onClick={() => { + trackEvent({ + action: 'button_click', + category: 'navigation', + label: 'explore_data', + }); + router.push('/explore-data'); + }} className="rounded-none" > Explore data diff --git a/src/website2/src/components/layouts/NewsLetter.tsx b/src/website2/src/components/layouts/NewsLetter.tsx index 8c8f93706c..da39b56ca9 100644 --- a/src/website2/src/components/layouts/NewsLetter.tsx +++ b/src/website2/src/components/layouts/NewsLetter.tsx @@ -5,6 +5,8 @@ import { CustomButton } from '@/components/ui'; import mainConfig from '@/configs/mainConfigs'; import { subscribeToNewsletter } from '@/services/externalService'; +import { trackEvent } from '../GoogleAnalytics'; + const NewsLetter: React.FC = () => { const [formStatus, setFormStatus] = useState<'idle' | 'success' | 'error'>( 'idle', @@ -32,6 +34,11 @@ const NewsLetter: React.FC = () => { const response = await subscribeToNewsletter(formData); if (response.success) { setFormStatus('success'); + trackEvent({ + action: 'submit_form', + category: 'newsletter', + label: 'newsletter_subscription', + }); } else { setFormStatus('error'); } @@ -71,7 +78,7 @@ const NewsLetter: React.FC = () => { // Split layout for idle state (header and form side-by-side on lg screens)
-

+

Subscribe to our Newsletter

diff --git a/src/website2/src/components/layouts/NotificationBanner.tsx b/src/website2/src/components/layouts/NotificationBanner.tsx index f80e02c09f..dd4348ad7d 100644 --- a/src/website2/src/components/layouts/NotificationBanner.tsx +++ b/src/website2/src/components/layouts/NotificationBanner.tsx @@ -10,7 +10,7 @@ import mainConfig from '@/configs/mainConfigs'; import { trackEvent } from '../GoogleAnalytics'; -const CLEAN_AIR_NETWORK_ROUTE = '/clean-air-network'; +const CLEAN_AIR_NETWORK_ROUTE = '/clean-air-network/about'; const NotificationBanner: React.FC = () => { const handleNetworkClick = (version: 'desktop' | 'mobile') => { @@ -52,7 +52,7 @@ const NotificationBanner: React.FC = () => {

{ - e.stopPropagation(); // Prevent double event firing + e.stopPropagation(); handleNetworkClick('mobile'); }} > diff --git a/src/website2/src/components/sections/footer/CountrySelectorDialog.tsx b/src/website2/src/components/sections/footer/CountrySelectorDialog.tsx index 2c7fa9e713..fd206c8dd9 100644 --- a/src/website2/src/components/sections/footer/CountrySelectorDialog.tsx +++ b/src/website2/src/components/sections/footer/CountrySelectorDialog.tsx @@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { FiChevronDown } from 'react-icons/fi'; import { IoLocationSharp } from 'react-icons/io5'; +import { trackEvent } from '@/components/GoogleAnalytics'; import { CustomButton, Dialog, @@ -224,7 +225,16 @@ const CountrySelectorDialog: React.FC = () => { return ( -
); }; diff --git a/src/website2/src/views/Forum/AboutPage.tsx b/src/website2/src/views/cleanairforum/about/AboutPage.tsx similarity index 94% rename from src/website2/src/views/Forum/AboutPage.tsx rename to src/website2/src/views/cleanairforum/about/AboutPage.tsx index c0ec9f38b3..a8112a636d 100644 --- a/src/website2/src/views/Forum/AboutPage.tsx +++ b/src/website2/src/views/cleanairforum/about/AboutPage.tsx @@ -7,7 +7,7 @@ import { Divider, NoData } from '@/components/ui'; import { useForumData } from '@/context/ForumDataContext'; import { isValidHTMLContent } from '@/utils/htmlValidator'; import { renderContent } from '@/utils/quillUtils'; -import SectionDisplay from '@/views/Forum/SectionDisplay'; +import SectionDisplay from '@/views/cleanairforum/SectionDisplay'; type SectionRowProps = { title: string; @@ -27,7 +27,7 @@ const SectionRow: React.FC = ({ title, children }) => ( ); -const AboutPage: React.FC = () => { +const AboutPage = () => { const { selectedEvent } = useForumData(); if (!selectedEvent) { diff --git a/src/website2/src/views/cleanairforum/glossary/GlossaryPage.tsx b/src/website2/src/views/cleanairforum/glossary/GlossaryPage.tsx new file mode 100644 index 0000000000..e4a9f3d637 --- /dev/null +++ b/src/website2/src/views/cleanairforum/glossary/GlossaryPage.tsx @@ -0,0 +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/cleanairforum/SectionDisplay'; + +const GlossaryPage = () => { + // 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/src/website2/src/views/cleanairforum/logistics/LogisticsPage.tsx b/src/website2/src/views/cleanairforum/logistics/LogisticsPage.tsx new file mode 100644 index 0000000000..dacaa3a9e1 --- /dev/null +++ b/src/website2/src/views/cleanairforum/logistics/LogisticsPage.tsx @@ -0,0 +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/cleanairforum/SectionDisplay'; + +const LogisticsPage = () => { + // 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/src/website2/src/views/cleanairforum/partners/PartnersPage.tsx b/src/website2/src/views/cleanairforum/partners/PartnersPage.tsx new file mode 100644 index 0000000000..00a70287b6 --- /dev/null +++ b/src/website2/src/views/cleanairforum/partners/PartnersPage.tsx @@ -0,0 +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 SectionDisplay from '@/views/cleanairforum/SectionDisplay'; +import PaginatedSection from '@/views/cleanAirNetwork/PaginatedSection'; + +const PartnersPage = () => { + 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/src/website2/src/views/cleanairforum/program-committee/CommitteePage.tsx b/src/website2/src/views/cleanairforum/program-committee/CommitteePage.tsx new file mode 100644 index 0000000000..dae9c51ee7 --- /dev/null +++ b/src/website2/src/views/cleanairforum/program-committee/CommitteePage.tsx @@ -0,0 +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/cleanairforum/SectionDisplay'; + +const CommitteePage = () => { + // 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/src/website2/src/views/cleanairforum/resources/ResourcesPage.tsx b/src/website2/src/views/cleanairforum/resources/ResourcesPage.tsx new file mode 100644 index 0000000000..e07c787345 --- /dev/null +++ b/src/website2/src/views/cleanairforum/resources/ResourcesPage.tsx @@ -0,0 +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 = () => { + 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/src/website2/src/views/cleanairforum/sessions-programs/ProgramsPage.tsx b/src/website2/src/views/cleanairforum/sessions-programs/ProgramsPage.tsx new file mode 100644 index 0000000000..904e9cf4b3 --- /dev/null +++ b/src/website2/src/views/cleanairforum/sessions-programs/ProgramsPage.tsx @@ -0,0 +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/cleanairforum/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 = () => { + 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/src/website2/src/views/cleanairforum/speakers/SpeakersPage.tsx b/src/website2/src/views/cleanairforum/speakers/SpeakersPage.tsx new file mode 100644 index 0000000000..b39f1a042c --- /dev/null +++ b/src/website2/src/views/cleanairforum/speakers/SpeakersPage.tsx @@ -0,0 +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/cleanairforum/SectionDisplay'; + +const SpeakersPage = () => { + // 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/src/website2/src/views/cleanairforum/sponsorship/SponsorshipPage.tsx b/src/website2/src/views/cleanairforum/sponsorship/SponsorshipPage.tsx new file mode 100644 index 0000000000..397a6e3e79 --- /dev/null +++ b/src/website2/src/views/cleanairforum/sponsorship/SponsorshipPage.tsx @@ -0,0 +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 SectionDisplay from '@/views/cleanairforum/SectionDisplay'; +import PaginatedSection from '@/views/cleanAirNetwork/PaginatedSection'; + +const SponsorshipPage = () => { + 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/src/website2/src/views/home/HomePlayerSection.tsx b/src/website2/src/views/home/HomePlayerSection.tsx index 102fa7629a..c30499e92b 100644 --- a/src/website2/src/views/home/HomePlayerSection.tsx +++ b/src/website2/src/views/home/HomePlayerSection.tsx @@ -192,6 +192,11 @@ const HomePlayerSection: React.FC = () => { const modalPlayerRef = useRef(null); const handlePlayButtonClick = useCallback(() => { + trackEvent({ + action: 'video_play', + category: 'video', + label: 'home_page_video', + }); setVideoState((prev) => ({ ...prev, isModalOpen: true })); }, []);