From bee8d6d8d57ec8273105002832f63fd1d8c23d8e Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Sun, 26 Jan 2025 19:59:11 +0300 Subject: [PATCH 1/8] fix file link on resources page --- website2/src/app/clean-air-forum/layout.tsx | 54 +++--- website2/src/hooks/useResourceFilter.ts | 41 ++++ website2/src/types/index.ts | 21 +++ website2/src/utils/string-utils.ts | 6 + .../src/views/cleanairforum/TabNavigation.tsx | 40 ++-- .../cleanairforum/resources/ResourceCard.tsx | 60 ++++++ .../cleanairforum/resources/ResourcePage.tsx | 175 +++++------------- 7 files changed, 225 insertions(+), 172 deletions(-) create mode 100644 website2/src/hooks/useResourceFilter.ts create mode 100644 website2/src/utils/string-utils.ts create mode 100644 website2/src/views/cleanairforum/resources/ResourceCard.tsx diff --git a/website2/src/app/clean-air-forum/layout.tsx b/website2/src/app/clean-air-forum/layout.tsx index 54f43a9fd1..8241bf6c22 100644 --- a/website2/src/app/clean-air-forum/layout.tsx +++ b/website2/src/app/clean-air-forum/layout.tsx @@ -1,7 +1,6 @@ 'use client'; import React, { ReactNode } from 'react'; -import { Suspense } from 'react'; import Footer from '@/components/layouts/Footer'; import Navbar from '@/components/layouts/Navbar'; @@ -17,37 +16,40 @@ type CleanAirLayoutProps = { const CleanAirLayout: React.FC = ({ children }) => { // Using the `useForumEvents` hook - const { forumEvents } = useForumEvents(); + const { forumEvents, isLoading } = useForumEvents(); // Extract the first event (if available) const eventData = forumEvents?.[0] || null; + // Loading state + if (isLoading) { + return ; + } + return ( - }> -
- {/* Navbar */} -
- -
- - {/* Banner Section */} - - - {/* Main Content */} -
{children}
- - {/* Newsletter Section */} -
- -
- - {/* Footer */} -
-
-
-
-
+
+ {/* Navbar */} +
+ +
+ + {/* Banner Section */} + + + {/* Main Content */} +
{children}
+ + {/* Newsletter Section */} +
+ +
+ + {/* Footer */} +
+
+
+
); }; diff --git a/website2/src/hooks/useResourceFilter.ts b/website2/src/hooks/useResourceFilter.ts new file mode 100644 index 0000000000..30f90ab8bc --- /dev/null +++ b/website2/src/hooks/useResourceFilter.ts @@ -0,0 +1,41 @@ +import { useMemo, useState } from 'react'; + +import type { Resource } from '@/types/index'; + +export const useResourceFilter = ( + resources: Resource[], + itemsPerPage: number, +) => { + const [selectedCategory, setSelectedCategory] = useState('All'); + const [currentPage, setCurrentPage] = useState(1); + + const filteredResources = useMemo(() => { + if (selectedCategory === 'All') return resources; + return resources.filter( + (resource) => + resource.resource_category === + selectedCategory.toLowerCase().replace(' ', '_'), + ); + }, [resources, selectedCategory]); + + const totalPages = Math.ceil(filteredResources.length / itemsPerPage); + + const paginatedResources = useMemo( + () => + filteredResources.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage, + ), + [filteredResources, currentPage, itemsPerPage], + ); + + return { + selectedCategory, + setSelectedCategory, + currentPage, + setCurrentPage, + filteredResources, + paginatedResources, + totalPages, + }; +}; diff --git a/website2/src/types/index.ts b/website2/src/types/index.ts index 85578ff142..b04173d81a 100644 --- a/website2/src/types/index.ts +++ b/website2/src/types/index.ts @@ -9,3 +9,24 @@ export interface PressArticle { website_category: string; article_tag: string; } + +export interface Resource { + id: number; + resource_file_url: string; + created: string; + modified: string; + is_deleted: boolean; + resource_title: string; + resource_link: string | null; + resource_file: string; + author_title: string; + resource_category: + | 'workshop_report' + | 'technical_report' + | 'toolkit' + | 'research_publication'; + resource_authors: string; + order: number; +} + +export type ResourceCategory = Resource['resource_category']; diff --git a/website2/src/utils/string-utils.ts b/website2/src/utils/string-utils.ts new file mode 100644 index 0000000000..6dd4fb6dd5 --- /dev/null +++ b/website2/src/utils/string-utils.ts @@ -0,0 +1,6 @@ +export const formatString = (category: string): string => { + return category + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +}; diff --git a/website2/src/views/cleanairforum/TabNavigation.tsx b/website2/src/views/cleanairforum/TabNavigation.tsx index d51687e4bd..28b50e024f 100644 --- a/website2/src/views/cleanairforum/TabNavigation.tsx +++ b/website2/src/views/cleanairforum/TabNavigation.tsx @@ -1,31 +1,33 @@ 'use client'; -import { usePathname, useRouter } from 'next/navigation'; -import React, { useState } from 'react'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import type React from 'react'; +import { useEffect, useState } from 'react'; + +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' }, +]; const TabNavigation: React.FC = () => { - const router = useRouter(); const pathname = usePathname(); const [activeTab, setActiveTab] = useState(pathname); - 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' }, - ]; - - const handleTabClick = (value: string) => { - setActiveTab(value); - router.push(value); - }; + useEffect(() => { + setActiveTab(pathname); + }, [pathname]); return ( -
+
+ ); }; diff --git a/website2/src/views/cleanairforum/resources/ResourceCard.tsx b/website2/src/views/cleanairforum/resources/ResourceCard.tsx new file mode 100644 index 0000000000..4fedfcb1c9 --- /dev/null +++ b/website2/src/views/cleanairforum/resources/ResourceCard.tsx @@ -0,0 +1,60 @@ +import type React from 'react'; + +import { CustomButton } from '@/components/ui'; +import type { Resource } from '@/types/index'; +import { formatString } from '@/utils/string-utils'; + +interface ResourceCardProps { + resource: Resource; +} + +const ResourceCard: React.FC = ({ resource }) => ( +
+

+ {formatString(resource.resource_category)} +

+

+ {resource.resource_title} +

+
+

{resource.author_title}

+

{resource.resource_authors}

+
+
+ {resource.resource_link && ( + { + if (resource.resource_link) { + window.open( + resource.resource_link, + '_blank', + 'noopener,noreferrer', + ); + } + }} + > + Read action plan → + + )} + {resource.resource_file && resource.resource_file_url && ( + { + if (resource.resource_file_url) { + window.open( + resource.resource_file_url, + '_blank', + 'noopener,noreferrer', + ); + } + }} + > + Download + + )} +
+
+); + +export default ResourceCard; diff --git a/website2/src/views/cleanairforum/resources/ResourcePage.tsx b/website2/src/views/cleanairforum/resources/ResourcePage.tsx index a2b8e7295d..0d31955895 100644 --- a/website2/src/views/cleanairforum/resources/ResourcePage.tsx +++ b/website2/src/views/cleanairforum/resources/ResourcePage.tsx @@ -1,10 +1,9 @@ 'use client'; import Image from 'next/image'; -import React, { useMemo, useState } from 'react'; +import type React from 'react'; import { - CustomButton, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -13,73 +12,48 @@ import { Pagination, } from '@/components/ui'; import { useCleanAirResources } from '@/hooks/useApiHooks'; - -const ResourcePage = () => { +import { useResourceFilter } from '@/hooks/useResourceFilter'; +import type { Resource } from '@/types/index'; + +import ResourceCard from './ResourceCard'; + +const categories = [ + 'All', + 'Toolkit', + 'Technical Report', + 'Workshop Report', + 'Research Publication', +]; + +const LoadingSkeleton = ({ itemsPerPage }: { itemsPerPage: number }) => ( +
+ {Array.from({ length: itemsPerPage }).map((_, index) => ( +
+
+
+
+
+
+
+ ))} +
+); + +const ResourcePage: React.FC = () => { const { cleanAirResources, isLoading, isError } = useCleanAirResources(); - const [selectedCategory, setSelectedCategory] = useState('All'); - const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 3; - // Categories for the dropdown filter - const categories = [ - 'All', - 'Toolkits', - 'Technical Reports', - 'Workshop Reports', - 'Research Publications', - ]; - - // Map and filter resources based on the selected category - const filteredResources = useMemo(() => { - if (selectedCategory === 'All') return cleanAirResources || []; - - const categoryMap: { [key: string]: string } = { - Toolkits: 'toolkit', - 'Technical Reports': 'technical_report', - 'Workshop Reports': 'workshop_report', - 'Research Publications': 'research_publication', - }; - - return ( - cleanAirResources?.filter( - (resource: any) => - resource.resource_category === categoryMap[selectedCategory], - ) || [] - ); - }, [cleanAirResources, selectedCategory]); - - // Pagination logic - const totalPages = Math.ceil(filteredResources.length / itemsPerPage); - const paginatedResources = useMemo( - () => - filteredResources.slice( - (currentPage - 1) * itemsPerPage, - currentPage * itemsPerPage, - ), - [filteredResources, currentPage, itemsPerPage], - ); - - // Loading Skeleton Component - const LoadingSkeleton = () => ( -
- {Array.from({ length: itemsPerPage }).map((_, index) => ( -
-
-
-
-
-
-
- ))} -
- ); + const { + selectedCategory, + setSelectedCategory, + currentPage, + setCurrentPage, + paginatedResources, + totalPages, + } = useResourceFilter(cleanAirResources || [], itemsPerPage); return (
- {/* Main banner section */}
@@ -89,19 +63,18 @@ const ResourcePage = () => { width={800} height={400} className="rounded-lg object-contain w-full" + priority />
- {/* Resource Filter and List */}

Resource Center

- {/* Filter Dropdown */}
- {/* Loading and Error States */} - {isLoading && } + {isLoading && } {isError && ( )} - {/* Resource Cards */} {!isLoading && !isError && paginatedResources.length === 0 && ( )} {!isLoading && !isError && paginatedResources.length > 0 && (
- {paginatedResources.map((resource: any, index: any) => ( -
-

- {resource.resource_category.toUpperCase()} -

-

- {resource.resource_title} -

-
-

Created by

-

{resource.resource_authors}

-
-
- {/* Read Action Plan Button */} - {resource.resource_link && ( - - window.open( - resource.resource_link, - '_blank', - 'noopener,noreferrer', - ) - } - > - Read action plan → - - )} - - {/* Download Button */} - {resource.resource_file && ( - - window.open( - resource.resource_file, - '_blank', - 'noopener,noreferrer', - ) - } - > - Download - - )} -
-
+ {paginatedResources.map((resource: Resource) => ( + ))}
)} - {/* Pagination */} - {!isLoading && - !isError && - filteredResources.length > itemsPerPage && ( - - )} + {!isLoading && !isError && totalPages > 1 && ( + + )}
From 219d2b4aa9d53247d109334a040b5a9917866397 Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Sun, 26 Jan 2025 20:00:23 +0300 Subject: [PATCH 2/8] text change --- website2/src/views/cleanairforum/resources/ResourceCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website2/src/views/cleanairforum/resources/ResourceCard.tsx b/website2/src/views/cleanairforum/resources/ResourceCard.tsx index 4fedfcb1c9..adb27ee6aa 100644 --- a/website2/src/views/cleanairforum/resources/ResourceCard.tsx +++ b/website2/src/views/cleanairforum/resources/ResourceCard.tsx @@ -34,7 +34,7 @@ const ResourceCard: React.FC = ({ resource }) => ( } }} > - Read action plan → + Read more → )} {resource.resource_file && resource.resource_file_url && ( From f80b4e5e53b11dada64925fa8c9997d96b07279f Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Sun, 26 Jan 2025 21:26:59 +0300 Subject: [PATCH 3/8] set up main config settings for whole website --- website2/src/app/legal/layout.tsx | 5 +- .../components/dialogs/EngagementDialog.tsx | 5 +- .../src/components/layouts/ActionButtons.tsx | 5 +- .../src/components/layouts/ActionButtons2.tsx | 6 +- website2/src/components/layouts/Footer.tsx | 4 +- website2/src/components/layouts/Highlight.tsx | 6 +- website2/src/components/layouts/Navbar.tsx | 5 +- .../src/components/layouts/NewsLetter.tsx | 5 +- .../components/layouts/NotificationBanner.tsx | 57 +-- website2/src/components/ui/Pagination.tsx | 61 ++- website2/src/configs/mainConfigs.ts | 13 + website2/src/views/about/AboutPage.tsx | 417 +++++++++--------- website2/src/views/careers/CareerPage.tsx | 215 ++++----- website2/src/views/careers/DetailsPage.tsx | 7 +- .../src/views/cleanairforum/FeaturedEvent.tsx | 9 +- .../views/cleanairforum/PaginatedSection.tsx | 3 +- .../views/cleanairforum/RegisterBanner.tsx | 3 +- .../src/views/cleanairforum/TabNavigation.tsx | 20 +- .../cleanairforum/resources/ResourcePage.tsx | 1 + .../src/views/events/EventCardsSection.tsx | 5 +- website2/src/views/events/EventPage.tsx | 21 +- website2/src/views/events/SingleEvent.tsx | 341 +++++++------- .../views/home/AnalyticsContentSection.tsx | 5 +- .../src/views/home/AppDownloadSection.tsx | 6 +- website2/src/views/home/FeaturedCarousel.tsx | 5 +- website2/src/views/home/HomePlayerSection.tsx | 5 +- website2/src/views/home/HomeStatsSection.tsx | 3 +- website2/src/views/legal/AirQoDataPage.tsx | 2 +- website2/src/views/legal/PP_Page.tsx | 2 +- website2/src/views/legal/PRP_Page.tsx | 2 +- website2/src/views/legal/TOSPage.tsx | 2 +- website2/src/views/legal/Tabsection.tsx | 46 +- website2/src/views/press/PressPage.tsx | 10 +- .../src/views/publications/ResourcePage.tsx | 6 +- .../solutions/AfricanCities/AfricanCities.tsx | 3 +- .../AfricanCities/AfricanCityPage.tsx | 5 +- .../solutions/communities/CommunityPage.tsx | 11 +- website2/tsconfig.json | 3 +- 38 files changed, 738 insertions(+), 592 deletions(-) create mode 100644 website2/src/configs/mainConfigs.ts diff --git a/website2/src/app/legal/layout.tsx b/website2/src/app/legal/layout.tsx index d712dc909d..09908370d0 100644 --- a/website2/src/app/legal/layout.tsx +++ b/website2/src/app/legal/layout.tsx @@ -2,6 +2,7 @@ import { Metadata } from 'next'; import React from 'react'; import MainLayout from '@/components/layouts/MainLayout'; +import mainConfig from '@/configs/mainConfigs'; import TabSection from '@/views/legal/Tabsection'; export const metadata: Metadata = { @@ -51,7 +52,9 @@ const LegalPageLayout: React.FC = ({ children }) => { return ( -
{children}
+
+ {children} +
); }; diff --git a/website2/src/components/dialogs/EngagementDialog.tsx b/website2/src/components/dialogs/EngagementDialog.tsx index 1aab405e97..0ad66e1528 100644 --- a/website2/src/components/dialogs/EngagementDialog.tsx +++ b/website2/src/components/dialogs/EngagementDialog.tsx @@ -5,6 +5,7 @@ import React, { useState } from 'react'; import { FiArrowLeft } from 'react-icons/fi'; import { Dialog, DialogContent } from '@/components/ui/dialog'; +import mainConfig from '@/configs/mainConfigs'; import { useDispatch, useSelector } from '@/hooks'; import { postContactUs } from '@/services/externalService'; import { closeModal } from '@/store/slices/modalSlice'; @@ -349,7 +350,9 @@ const EngagementDialog = () => { return ( - +
{/* Left Side - Breadcrumb and Text with Animation */} { const router = useRouter(); const dispatch = useDispatch(); return ( -
+
{/* Card 1 */} { diff --git a/website2/src/components/layouts/ActionButtons2.tsx b/website2/src/components/layouts/ActionButtons2.tsx index 67883d54eb..ae6f58f9ec 100644 --- a/website2/src/components/layouts/ActionButtons2.tsx +++ b/website2/src/components/layouts/ActionButtons2.tsx @@ -1,11 +1,15 @@ 'use client'; import React from 'react'; +import mainConfig from '@/configs/mainConfigs'; + import { CustomButton } from '../ui'; const ActionButtons2 = () => { return ( -
+
{/* Card 1 */} { diff --git a/website2/src/components/layouts/Footer.tsx b/website2/src/components/layouts/Footer.tsx index 1bff44d4c6..24c76d3f8e 100644 --- a/website2/src/components/layouts/Footer.tsx +++ b/website2/src/components/layouts/Footer.tsx @@ -8,6 +8,8 @@ import { FaYoutube, } from 'react-icons/fa'; +import mainConfig from '@/configs/mainConfigs'; + import CountrySelectorDialog from '../sections/footer/CountrySelectorDialog'; import MonitorDisplay from '../sections/footer/MonitorDisplay'; import ScrollToTopButton from './ScrollToTopButton'; @@ -17,7 +19,7 @@ const Footer = () => { return (