- We'll streamline your setup experience accordingly
+ We'll streamline your setup experience accordingly
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 && (
-
- )}
-
- {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 && (
- <>
-
-
- >
- )}
+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) => (
-
- ))}
-
- )}
-
- );
+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 (
-
-
-
- Expand All
-
-
- Collapse All
-
-
-
- {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 (
-
-
- {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 && (
-
- )}
-
- {sessionSections && sessionSections.length > 0 && (
- <>
- {sessionSections.map((section: any) => (
-
- ))}
- >
- )}
-
- <>
- {selectedEvent.programs?.map((program: any) => (
-
handleToggle(program.id)}
- />
- ))}
- >
-
- {showRegistration && (
-
- )}
-
- );
+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 && (
- <>
-
-
- >
- )}
-
- );
+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 (
-
+ {
+ trackEvent({
+ action: 'button_click',
+ category: 'navigation',
+ label: 'country_selector',
+ });
+ }}
+ className="flex items-center space-x-2 px-4 py-2 bg-gray-100 rounded-xl text-gray-800 hover:bg-gray-200 transition-all"
+ >
{selectedCountryData?.long_name.replace('_', ' ') ||
diff --git a/src/website2/src/views/Forum/TabNavigation.tsx b/src/website2/src/views/Forum/TabNavigation.tsx
deleted file mode 100644
index e34eeb07dc..0000000000
--- a/src/website2/src/views/Forum/TabNavigation.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-// components/layouts/TabNavigation.tsx
-'use client';
-
-import Link from 'next/link';
-import { usePathname, useSearchParams } from 'next/navigation';
-import React from 'react';
-
-const TabNavigation: React.FC = () => {
- const pathname = usePathname();
- const searchParams = useSearchParams();
- // Read the current slug, if it exists.
- const currentSlug = searchParams.get('slug');
-
- // Function to check if the tab is active based on the current pathname.
- const isActiveTab = (path: string) =>
- pathname === path || pathname.startsWith(path);
-
- // Define the tabs list.
- const tabs = [
- { href: '/clean-air-forum/about', text: 'About' },
- { href: '/clean-air-forum/program-committee', text: 'Programme Committee' },
- { href: '/clean-air-forum/sessions', text: 'Call for Sessions' },
- { href: '/clean-air-forum/speakers', text: 'Speakers' },
- { href: '/clean-air-forum/partners', text: 'Partners' },
- { href: '/clean-air-forum/sponsorships', text: 'Sponsorships' },
- { href: '/clean-air-forum/logistics', text: 'Travel Logistics' },
- { href: '/clean-air-forum/glossary', text: 'Glossary' },
- { href: '/clean-air-forum/resources', text: 'Resources' },
- ];
-
- // If currentSlug exists, append it to the URL as a query parameter.
- const buildHref = (href: string) => {
- if (currentSlug) {
- return `${href}?slug=${encodeURIComponent(currentSlug)}`;
- }
- return href;
- };
-
- return (
-
-
-
- {tabs.map((link, index) => (
-
- {link.text}
- {isActiveTab(link.href) && (
-
- )}
-
- ))}
-
-
-
- );
-};
-
-export default TabNavigation;
diff --git a/src/website2/src/views/about/AboutPage.tsx b/src/website2/src/views/about/AboutPage.tsx
index 55da3ea15e..c6ffd06b9e 100644
--- a/src/website2/src/views/about/AboutPage.tsx
+++ b/src/website2/src/views/about/AboutPage.tsx
@@ -15,7 +15,7 @@ import {
useTeamMembers,
} from '@/hooks/useApiHooks';
import { openModal } from '@/store/slices/modalSlice';
-import PaginatedSection from '@/views/cleanairforum/PaginatedSection';
+import PaginatedSection from '@/views/cleanAirNetwork/PaginatedSection';
/** Skeleton Loader Component **/
diff --git a/src/website2/src/views/cleanairforum/ContentSection.tsx b/src/website2/src/views/cleanAirNetwork/ContentSection.tsx
similarity index 100%
rename from src/website2/src/views/cleanairforum/ContentSection.tsx
rename to src/website2/src/views/cleanAirNetwork/ContentSection.tsx
diff --git a/src/website2/src/views/cleanairforum/EventCard.tsx b/src/website2/src/views/cleanAirNetwork/EventCard.tsx
similarity index 100%
rename from src/website2/src/views/cleanairforum/EventCard.tsx
rename to src/website2/src/views/cleanAirNetwork/EventCard.tsx
diff --git a/src/website2/src/views/cleanairforum/EventSkeleton.tsx b/src/website2/src/views/cleanAirNetwork/EventSkeleton.tsx
similarity index 100%
rename from src/website2/src/views/cleanairforum/EventSkeleton.tsx
rename to src/website2/src/views/cleanAirNetwork/EventSkeleton.tsx
diff --git a/src/website2/src/views/cleanairforum/FeaturedEvent.tsx b/src/website2/src/views/cleanAirNetwork/FeaturedEvent.tsx
similarity index 100%
rename from src/website2/src/views/cleanairforum/FeaturedEvent.tsx
rename to src/website2/src/views/cleanAirNetwork/FeaturedEvent.tsx
diff --git a/src/website2/src/views/cleanairforum/PaginatedSection.tsx b/src/website2/src/views/cleanAirNetwork/PaginatedSection.tsx
similarity index 100%
rename from src/website2/src/views/cleanairforum/PaginatedSection.tsx
rename to src/website2/src/views/cleanAirNetwork/PaginatedSection.tsx
diff --git a/src/website2/src/views/cleanairforum/RegisterBanner.tsx b/src/website2/src/views/cleanAirNetwork/RegisterBanner.tsx
similarity index 100%
rename from src/website2/src/views/cleanairforum/RegisterBanner.tsx
rename to src/website2/src/views/cleanAirNetwork/RegisterBanner.tsx
diff --git a/src/website2/src/views/cleanAirNetwork/TabNavigation.tsx b/src/website2/src/views/cleanAirNetwork/TabNavigation.tsx
new file mode 100644
index 0000000000..8733cdeac7
--- /dev/null
+++ b/src/website2/src/views/cleanAirNetwork/TabNavigation.tsx
@@ -0,0 +1,49 @@
+'use client';
+
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+import type React from 'react';
+
+import mainConfig from '@/configs/mainConfigs';
+
+const tabs = [
+ { label: 'About', value: '/clean-air-network/about' },
+ { label: 'Membership', value: '/clean-air-network/membership' },
+ { label: 'Events', value: '/clean-air-network/events' },
+ { label: 'Resources', value: '/clean-air-network/resources' },
+];
+
+const TabNavigation: React.FC = () => {
+ const pathname = usePathname();
+
+ const isActiveTab = (tabValue: string) => {
+ if (tabValue === '/clean-air-network/about') {
+ return pathname === tabValue;
+ }
+ return pathname.startsWith(tabValue);
+ };
+
+ return (
+
+
+ {tabs.map((tab) => (
+
+ {tab.label}
+
+ ))}
+
+
+ );
+};
+
+export default TabNavigation;
diff --git a/src/website2/src/app/clean-air-network/CleanAirPage.tsx b/src/website2/src/views/cleanAirNetwork/about/CleanAirPage.tsx
similarity index 95%
rename from src/website2/src/app/clean-air-network/CleanAirPage.tsx
rename to src/website2/src/views/cleanAirNetwork/about/CleanAirPage.tsx
index cd9601bc89..8b58f6e837 100644
--- a/src/website2/src/app/clean-air-network/CleanAirPage.tsx
+++ b/src/website2/src/views/cleanAirNetwork/about/CleanAirPage.tsx
@@ -2,8 +2,8 @@
import Image from 'next/image';
import React from 'react';
-import ContentSection from '@/views/cleanairforum/ContentSection';
-import FeaturedEvent from '@/views/cleanairforum/FeaturedEvent';
+import ContentSection from '@/views/cleanAirNetwork/ContentSection';
+import FeaturedEvent from '@/views/cleanAirNetwork/FeaturedEvent';
const CleanAirPage = () => {
const goals = [
diff --git a/src/website2/src/views/cleanairforum/events/EventsPage.tsx b/src/website2/src/views/cleanAirNetwork/events/EventsPage.tsx
similarity index 95%
rename from src/website2/src/views/cleanairforum/events/EventsPage.tsx
rename to src/website2/src/views/cleanAirNetwork/events/EventsPage.tsx
index b0b0302d78..d3d5e434c7 100644
--- a/src/website2/src/views/cleanairforum/events/EventsPage.tsx
+++ b/src/website2/src/views/cleanAirNetwork/events/EventsPage.tsx
@@ -14,9 +14,9 @@ import {
} from '@/components/ui';
import mainConfig from '@/configs/mainConfigs';
import { useCleanAirEvents } from '@/hooks/useApiHooks';
-import EventCard from '@/views/cleanairforum/EventCard';
-import EventSkeleton from '@/views/cleanairforum/EventSkeleton';
-import RegisterBanner from '@/views/cleanairforum/RegisterBanner';
+import EventCard from '@/views/cleanAirNetwork/EventCard';
+import EventSkeleton from '@/views/cleanAirNetwork/EventSkeleton';
+import RegisterBanner from '@/views/cleanAirNetwork/RegisterBanner';
const months = [
'January',
diff --git a/src/website2/src/views/cleanairforum/events/SingleEvent.tsx b/src/website2/src/views/cleanAirNetwork/events/SingleEvent.tsx
similarity index 100%
rename from src/website2/src/views/cleanairforum/events/SingleEvent.tsx
rename to src/website2/src/views/cleanAirNetwork/events/SingleEvent.tsx
diff --git a/src/website2/src/views/cleanairforum/membership/MemberPage.tsx b/src/website2/src/views/cleanAirNetwork/membership/MemberPage.tsx
similarity index 95%
rename from src/website2/src/views/cleanairforum/membership/MemberPage.tsx
rename to src/website2/src/views/cleanAirNetwork/membership/MemberPage.tsx
index 277bfb2a6e..8ee68149d2 100644
--- a/src/website2/src/views/cleanairforum/membership/MemberPage.tsx
+++ b/src/website2/src/views/cleanAirNetwork/membership/MemberPage.tsx
@@ -5,8 +5,8 @@ import React, { useMemo } from 'react';
import mainConfig from '@/configs/mainConfigs';
import { usePartners } from '@/hooks/useApiHooks';
-import PaginatedSection from '@/views/cleanairforum/PaginatedSection';
-import RegisterBanner from '@/views/cleanairforum/RegisterBanner';
+import PaginatedSection from '@/views/cleanAirNetwork/PaginatedSection';
+import RegisterBanner from '@/views/cleanAirNetwork/RegisterBanner';
const SkeletonPaginatedSection: React.FC = () => {
return (
diff --git a/src/website2/src/views/cleanairforum/resources/ResourceCard.tsx b/src/website2/src/views/cleanAirNetwork/resources/ResourceCard.tsx
similarity index 100%
rename from src/website2/src/views/cleanairforum/resources/ResourceCard.tsx
rename to src/website2/src/views/cleanAirNetwork/resources/ResourceCard.tsx
diff --git a/src/website2/src/views/cleanairforum/resources/ResourcePage.tsx b/src/website2/src/views/cleanAirNetwork/resources/ResourcePage.tsx
similarity index 100%
rename from src/website2/src/views/cleanairforum/resources/ResourcePage.tsx
rename to src/website2/src/views/cleanAirNetwork/resources/ResourcePage.tsx
diff --git a/src/website2/src/views/Forum/BannerSection.tsx b/src/website2/src/views/cleanairforum/BannerSection.tsx
similarity index 92%
rename from src/website2/src/views/Forum/BannerSection.tsx
rename to src/website2/src/views/cleanairforum/BannerSection.tsx
index e7886bf2dc..c201a56a96 100644
--- a/src/website2/src/views/Forum/BannerSection.tsx
+++ b/src/website2/src/views/cleanairforum/BannerSection.tsx
@@ -8,11 +8,7 @@ import { CustomButton } from '@/components/ui';
import TabNavigation from './TabNavigation';
-type BannerSectionProps = {
- data: any; // Type it accordingly
-};
-
-const BannerSection: React.FC = ({ data }) => {
+const BannerSection = ({ data }: { data: any }) => {
if (!data) {
return null;
}
diff --git a/src/website2/src/views/Forum/SectionDisplay.tsx b/src/website2/src/views/cleanairforum/SectionDisplay.tsx
similarity index 100%
rename from src/website2/src/views/Forum/SectionDisplay.tsx
rename to src/website2/src/views/cleanairforum/SectionDisplay.tsx
diff --git a/src/website2/src/views/cleanairforum/TabNavigation.tsx b/src/website2/src/views/cleanairforum/TabNavigation.tsx
index 9fb60cf74e..e34eeb07dc 100644
--- a/src/website2/src/views/cleanairforum/TabNavigation.tsx
+++ b/src/website2/src/views/cleanairforum/TabNavigation.tsx
@@ -1,48 +1,62 @@
+// components/layouts/TabNavigation.tsx
'use client';
import Link from 'next/link';
-import { usePathname } from 'next/navigation';
-import type React from 'react';
-
-import mainConfig from '@/configs/mainConfigs';
-
-const tabs = [
- { label: 'About', value: '/clean-air-network' },
- { label: 'Membership', value: '/clean-air-network/membership' },
- { label: 'Events', value: '/clean-air-network/events' },
- { label: 'Resources', value: '/clean-air-network/resources' },
-];
+import { usePathname, useSearchParams } from 'next/navigation';
+import React from 'react';
const TabNavigation: React.FC = () => {
const pathname = usePathname();
+ const searchParams = useSearchParams();
+ // Read the current slug, if it exists.
+ const currentSlug = searchParams.get('slug');
+
+ // Function to check if the tab is active based on the current pathname.
+ const isActiveTab = (path: string) =>
+ pathname === path || pathname.startsWith(path);
+
+ // Define the tabs list.
+ const tabs = [
+ { href: '/clean-air-forum/about', text: 'About' },
+ { href: '/clean-air-forum/program-committee', text: 'Programme Committee' },
+ { href: '/clean-air-forum/sessions', text: 'Call for Sessions' },
+ { href: '/clean-air-forum/speakers', text: 'Speakers' },
+ { href: '/clean-air-forum/partners', text: 'Partners' },
+ { href: '/clean-air-forum/sponsorships', text: 'Sponsorships' },
+ { href: '/clean-air-forum/logistics', text: 'Travel Logistics' },
+ { href: '/clean-air-forum/glossary', text: 'Glossary' },
+ { href: '/clean-air-forum/resources', text: 'Resources' },
+ ];
- const isActiveTab = (tabValue: string) => {
- if (tabValue === '/clean-air-network') {
- return pathname === tabValue;
+ // If currentSlug exists, append it to the URL as a query parameter.
+ const buildHref = (href: string) => {
+ if (currentSlug) {
+ return `${href}?slug=${encodeURIComponent(currentSlug)}`;
}
- return pathname.startsWith(tabValue);
+ return href;
};
return (
-
-
- {tabs.map((tab) => (
-
- {tab.label}
-
- ))}
+
+
+
+ {tabs.map((link, index) => (
+
+ {link.text}
+ {isActiveTab(link.href) && (
+
+ )}
+
+ ))}
+
-
+
);
};
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 && (
+
+ )}
+
+ {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 && (
+ <>
+
+
+ >
+ )}
+
+ {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) => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+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 (
+
+
+
+ Expand All
+
+
+ Collapse All
+
+
+
+ {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 (
+
+
+ {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 && (
+
+ )}
+
+ {sessionSections && sessionSections.length > 0 && (
+ <>
+ {sessionSections.map((section: any) => (
+
+ ))}
+ >
+ )}
+
+ <>
+ {selectedEvent.programs?.map((program: any) => (
+
handleToggle(program.id)}
+ />
+ ))}
+ >
+
+ {showRegistration && (
+
+ )}
+
+ );
+};
+
+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 && (
+ <>
+
+
+ >
+ )}
+
+ );
+};
+
+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 }));
}, []);