diff --git a/website2/src/app/clean-air-forum/layout.tsx b/website2/src/app/clean-air-forum/layout.tsx index 4b28ada2d7..de10122c07 100644 --- a/website2/src/app/clean-air-forum/layout.tsx +++ b/website2/src/app/clean-air-forum/layout.tsx @@ -17,7 +17,7 @@ type CleanAirLayoutProps = { const CleanAirLayout: React.FC = ({ children }) => { // Using the `useForumEvents` hook - const { forumEvents, isLoading } = useForumEvents(); + const { data: forumEvents, isLoading } = useForumEvents(); // Extract the first event (if available) const eventData = forumEvents?.[0] || null; diff --git a/website2/src/app/partners/[id]/page.tsx b/website2/src/app/partners/[id]/page.tsx index b4770e7dbb..c8785262f3 100644 --- a/website2/src/app/partners/[id]/page.tsx +++ b/website2/src/app/partners/[id]/page.tsx @@ -10,7 +10,7 @@ const PartnerDetailsPage: React.FC = () => { const router = useRouter(); const params = useParams(); const { id } = params as { id: string }; - const { partnerDetails: partner, isLoading, isError } = usePartnerDetails(id); + const { data: partner, isLoading, isError } = usePartnerDetails(id); // Skeleton loader component const SkeletonLoader = () => ( diff --git a/website2/src/components/layouts/Navbar.tsx b/website2/src/components/layouts/Navbar.tsx index b29651c4df..a304a7df1b 100644 --- a/website2/src/components/layouts/Navbar.tsx +++ b/website2/src/components/layouts/Navbar.tsx @@ -190,7 +190,11 @@ const Navbar: React.FC = () => { className={`flex items-center justify-between ${mainConfig.containerClass}`} > {/* Logo Section */} - + AirQo Promise, + initialData: any = [], +) => { + const { data, error, mutate } = useSWR(key, fetcher, swrOptions); + const isLoading = !data && !error; -export function useImpactNumbers() { - const { data, error, isLoading, mutate } = useSWR( - 'impactNumbers', - getImpactNumbers, - swrOptions, - ); return { - impactNumbers: data || {}, + data: data ?? initialData, isLoading, isError: error, mutate, }; -} +}; -export function useAirQoEvents() { - const { data, error, isLoading, mutate } = useSWR( - 'airQoEvents', - getAirQoEvents, - swrOptions, - ); - return { - airQoEvents: data || [], - isLoading, - isError: error, - mutate, - }; -} +/** ------------------------------------- + * PRESS ARTICLES + * ------------------------------------- */ +export const usePressArticles = () => + useFetch('pressArticles', getPressArticles, []); -export function useCleanAirEvents() { - const { data, error, isLoading, mutate } = useSWR( - 'cleanAirEvents', - getCleanAirEvents, - swrOptions, - ); - return { - cleanAirEvents: data || [], - isLoading, - isError: error, - mutate, - }; -} - -export function useEventDetails(id: any) { - const { data, error, isLoading, mutate } = useSWR( - id ? `eventDetails/${id}` : null, - () => getEventDetails(id), - swrOptions, - ); - return { - eventDetails: data, - isLoading, - isError: error, - mutate, - }; -} +/** ------------------------------------- + * IMPACT NUMBERS + * ------------------------------------- */ +export const useImpactNumbers = () => + useFetch('impactNumbers', getImpactNumbers, {}); -export function useHighlights() { - const { data, error, isLoading, mutate } = useSWR( - 'highlights', - getHighlights, - swrOptions, - ); - return { - highlights: data || [], - isLoading, - isError: error, - mutate, - }; -} - -export function useCareers() { - const { data, error, isLoading, mutate } = useSWR( - 'careers', - getCareers, - swrOptions, - ); - return { - careers: data || [], - isLoading, - isError: error, - mutate, - }; -} - -export function useCareerDetails(id: any) { - const { data, error, isLoading, mutate } = useSWR( - id ? `careerDetails/${id}` : null, - () => getCareerDetails(id), - swrOptions, - ); - return { - careerDetails: data, - isLoading, - isError: error, - mutate, - }; -} - -export function useDepartments() { - const { data, error, isLoading, mutate } = useSWR( - 'departments', - getDepartments, - swrOptions, - ); - return { - departments: data || [], - isLoading, - isError: error, - mutate, - }; -} - -export function usePublications() { - const { data, error, isLoading, mutate } = useSWR( - 'publications', - getPublications, - swrOptions, - ); - return { - publications: data || [], - isLoading, - isError: error, - mutate, - }; -} +/** ------------------------------------- + * EVENTS + * ------------------------------------- */ +export const useAirQoEvents = () => useFetch('airQoEvents', getAirQoEvents, []); -export function useBoardMembers() { - const { data, error, isLoading, mutate } = useSWR( - 'boardMembers', - getBoardMembers, - swrOptions, - ); - return { - boardMembers: data || [], - isLoading, - isError: error, - mutate, - }; -} +export const useCleanAirEvents = () => + useFetch('cleanAirEvents', getCleanAirEvents, []); -export function useTeamMembers() { - const { data, error, isLoading, mutate } = useSWR( - 'teamMembers', - getTeamMembers, - swrOptions, - ); - return { - teamMembers: data || [], - isLoading, - isError: error, - mutate, - }; -} +export const useEventDetails = (id: string | null) => + useFetch(id ? `eventDetails/${id}` : null, () => getEventDetails(id!), null); -export function useExternalTeamMembers() { - const { data, error, isLoading, mutate } = useSWR( - 'externalTeamMembers', - getExternalTeamMembers, - swrOptions, - ); - return { - externalTeamMembers: data || [], - isLoading, - isError: error, - mutate, - }; -} +/** ------------------------------------- + * HIGHLIGHTS + * ------------------------------------- */ +export const useHighlights = () => useFetch('highlights', getHighlights, []); -export function usePartners() { - const { data, error, isLoading, mutate } = useSWR( - 'partners', - getPartners, - swrOptions, - ); - return { - partners: data || [], - isLoading, - isError: error, - mutate, - }; -} +/** ------------------------------------- + * CAREERS + * ------------------------------------- */ +export const useCareers = () => useFetch('careers', getCareers, []); -export function usePartnerDetails(id: any) { - const { data, error, isLoading, mutate } = useSWR( +export const useCareerDetails = (id: string | null) => + useFetch( + id ? `careerDetails/${id}` : null, + () => getCareerDetails(id!), + null, + ); + +/** ------------------------------------- + * DEPARTMENTS + * ------------------------------------- */ +export const useDepartments = () => useFetch('departments', getDepartments, []); + +/** ------------------------------------- + * PUBLICATIONS + * ------------------------------------- */ +export const usePublications = () => + useFetch('publications', getPublications, []); + +/** ------------------------------------- + * BOARD MEMBERS + * ------------------------------------- */ +export const useBoardMembers = () => + useFetch('boardMembers', getBoardMembers, []); + +/** ------------------------------------- + * TEAM MEMBERS + * ------------------------------------- */ +export const useTeamMembers = () => useFetch('teamMembers', getTeamMembers, []); + +export const useExternalTeamMembers = () => + useFetch('externalTeamMembers', getExternalTeamMembers, []); + +/** ------------------------------------- + * PARTNERS + * ------------------------------------- */ +export const usePartners = () => useFetch('partners', getPartners, []); + +export const usePartnerDetails = (id: string | null) => + useFetch( id ? `partnerDetails/${id}` : null, - () => getPartnerDetails(id), - swrOptions, + () => getPartnerDetails(id!), + null, ); - return { - partnerDetails: data, - isLoading, - isError: error, - mutate, - }; -} -export function useForumEvents() { - const { data, error, isLoading, mutate } = useSWR( - 'forumEvents', - getForumEvents, - swrOptions, - ); - return { - forumEvents: data || [], - isLoading, - isError: error, - mutate, - }; -} +/** ------------------------------------- + * FORUM EVENTS + * ------------------------------------- */ +export const useForumEvents = () => useFetch('forumEvents', getForumEvents, []); -export function useForumEventDetails(id: any) { - const { data, error, isLoading, mutate } = useSWR( +export const useForumEventDetails = (id: string | null) => + useFetch( id ? `forumEventDetails/${id}` : null, - () => getForumEventDetails(id), - swrOptions, + () => getForumEventDetails(id!), + null, ); - return { - forumEventDetails: data, - isLoading, - isError: error, - mutate, - }; -} -export function useCleanAirResources() { - const { data, error, isLoading, mutate } = useSWR( - 'cleanAirResources', - getCleanAirResources, - swrOptions, - ); - return { - cleanAirResources: data || [], - isLoading, - isError: error, - mutate, - }; -} +/** ------------------------------------- + * CLEAN AIR RESOURCES + * ------------------------------------- */ +export const useCleanAirResources = () => + useFetch('cleanAirResources', getCleanAirResources, []); -export function useAfricanCountries() { - const { data, error, isLoading, mutate } = useSWR( - 'africanCountries', - getAfricanCountries, - swrOptions, - ); - return { - africanCountries: data || [], - isLoading, - isError: error, - mutate, - }; -} +/** ------------------------------------- + * AFRICAN COUNTRIES + * ------------------------------------- */ +export const useAfricanCountries = () => + useFetch('africanCountries', getAfricanCountries, []); diff --git a/website2/src/services/apiService/index.tsx b/website2/src/services/apiService/index.tsx index 9b9b1351af..a3dfb4bea9 100644 --- a/website2/src/services/apiService/index.tsx +++ b/website2/src/services/apiService/index.tsx @@ -1,223 +1,115 @@ -import axios from 'axios'; +import axios, { AxiosError, AxiosInstance } from 'axios'; import { removeTrailingSlash } from '@/utils'; +// Define the base URL for the API const API_BASE_URL = `${removeTrailingSlash(process.env.NEXT_PUBLIC_API_URL || '')}/website`; -// Axios instance to include any necessary headers -const apiClient = axios.create({ +// Create an Axios instance with default configurations +const apiClient: AxiosInstance = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }); -// Function to fetch press articles -export const getPressArticles = async () => { +// Generic GET request handler +const getRequest = async (endpoint: string): Promise => { try { - const response = await apiClient.get('/press/'); + const response = await apiClient.get(endpoint); return response.data; } catch (error) { - console.error('Error fetching press articles:', error); - throw error; + const axiosError = error as AxiosError; + console.error(`Error fetching data from ${endpoint}:`, axiosError.message); + throw axiosError; } }; -// Function to fetch impact numbers -export const getImpactNumbers = async () => { - try { - const response = await apiClient.get('/impact-number/'); - return response.data; - } catch (error) { - console.error('Error fetching impact numbers:', error); - throw error; - } +// Press Articles API +export const getPressArticles = async (): Promise => { + return getRequest('/press/'); }; -// Function to fetch all events -export const getAirQoEvents = async () => { - try { - const response = await apiClient.get('/events/?category=airqo'); - return response.data; - } catch (error) { - console.error('Error fetching events:', error); - throw error; - } +// Impact Numbers API +export const getImpactNumbers = async (): Promise => { + return getRequest('/impact-number/'); }; -// Function to fetch all clean air events -export const getCleanAirEvents = async () => { - try { - const response = await apiClient.get('/events/?category=cleanair'); - return response.data; - } catch (error) { - console.error('Error fetching events:', error); - throw error; - } +// Events API +export const getAirQoEvents = async (): Promise => { + return getRequest('/events/?category=airqo'); }; -// Function to fetch a single event by ID -export const getEventDetails = async (id: string) => { - try { - const response = await apiClient.get(`/events/${id}/`); - return response.data; - } catch (error) { - console.error('Error fetching event details:', error); - throw error; - } +export const getCleanAirEvents = async (): Promise => { + return getRequest('/events/?category=cleanair'); }; -// Function to fetch highlights -export const getHighlights = async () => { - try { - const response = await apiClient.get('/highlights/'); - return response.data; - } catch (error) { - console.error('Error fetching highlights:', error); - throw error; - } +export const getEventDetails = async (id: string): Promise => { + return getRequest(`/events/${id}/`); }; -// Function to fetch careers -export const getCareers = async () => { - try { - const response = await apiClient.get('/careers/'); - return response.data; - } catch (error) { - console.error('Error fetching careers:', error); - throw error; - } +// Highlights API +export const getHighlights = async (): Promise => { + return getRequest('/highlights/'); }; -// Function to fetch a single career by ID -export const getCareerDetails = async (id: string) => { - try { - const response = await apiClient.get(`/careers/${id}/`); - return response.data; - } catch (error) { - console.error('Error fetching career details:', error); - throw error; - } +// Careers API +export const getCareers = async (): Promise => { + return getRequest('/careers/'); }; -// Function to get departments -export const getDepartments = async () => { - try { - const response = await apiClient.get('/departments/'); - return response.data; - } catch (error) { - console.error('Error fetching departments:', error); - throw error; - } +export const getCareerDetails = async (id: string): Promise => { + return getRequest(`/careers/${id}/`); }; -// Function to get publications -export const getPublications = async () => { - try { - const response = await apiClient.get('/publications/'); - return response.data; - } catch (error) { - console.error('Error fetching publications:', error); - throw error; - } +// Departments API +export const getDepartments = async (): Promise => { + return getRequest('/departments/'); }; -// Function to get board members -export const getBoardMembers = async () => { - try { - const response = await apiClient.get('/board-members/'); - return response.data; - } catch (error) { - console.error('Error fetching board members:', error); - throw error; - } +// Publications API +export const getPublications = async (): Promise => { + return getRequest('/publications/'); }; -// Function to get team members -export const getTeamMembers = async () => { - try { - const response = await apiClient.get('/team/'); - return response.data; - } catch (error) { - console.error('Error fetching team members:', error); - throw error; - } +// Board Members API +export const getBoardMembers = async (): Promise => { + return getRequest('/board-members/'); }; -// Function to get external team members -export const getExternalTeamMembers = async () => { - try { - const response = await apiClient.get('/external-team-members/'); - return response.data; - } catch (error) { - console.error('Error fetching external team members:', error); - throw error; - } +// Team Members API +export const getTeamMembers = async (): Promise => { + return getRequest('/team/'); }; -// Functiont to get partners -export const getPartners = async () => { - try { - const response = await apiClient.get('/partners/'); - return response.data; - } catch (error) { - console.error('Error fetching partners:', error); - throw error; - } +export const getExternalTeamMembers = async (): Promise => { + return getRequest('/external-team-members/'); }; -// function to get a single partner by ID +// Partners API +export const getPartners = async (): Promise => { + return getRequest('/partners/'); +}; -export const getPartnerDetails = async (id: string) => { - try { - const response = await apiClient.get(`/partners/${id}/`); - return response.data; - } catch (error) { - console.error('Error fetching partner details:', error); - throw error; - } +export const getPartnerDetails = async (id: string): Promise => { + return getRequest(`/partners/${id}/`); }; -// Function to get Forum events -export const getForumEvents = async () => { - try { - const response = await apiClient.get('/forum-events/'); - return response.data; - } catch (error) { - console.error('Error fetching forum events:', error); - throw error; - } +// Forum Events API +export const getForumEvents = async (): Promise => { + return getRequest('/forum-events/'); }; -// Function to get a Forum event by ID -export const getForumEventDetails = async (id: string) => { - try { - const response = await apiClient.get(`/forum-events/${id}/`); - return response.data; - } catch (error) { - console.error('Error fetching forum event details:', error); - throw error; - } +export const getForumEventDetails = async (id: string): Promise => { + return getRequest(`/forum-events/${id}/`); }; -// Function to get clean air resources -export const getCleanAirResources = async () => { - try { - const response = await apiClient.get('/clean-air-resources/'); - return response.data; - } catch (error) { - console.error('Error fetching clean air resources:', error); - throw error; - } +// Clean Air Resources API +export const getCleanAirResources = async (): Promise => { + return getRequest('/clean-air-resources/'); }; -// Function to get african-countries -export const getAfricanCountries = async () => { - try { - const response = await apiClient.get('/african-countries/'); - return response.data; - } catch (error) { - console.error('Error fetching african-countries:', error); - throw error; - } +// African Countries API +export const getAfricanCountries = async (): Promise => { + return getRequest('/african-countries/'); }; diff --git a/website2/src/services/externalService/index.tsx b/website2/src/services/externalService/index.tsx index 1d415b9463..d7a3f7745c 100644 --- a/website2/src/services/externalService/index.tsx +++ b/website2/src/services/externalService/index.tsx @@ -1,63 +1,120 @@ -import axios from 'axios'; +import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; import { removeTrailingSlash } from '@/utils'; +// ---------------------- +// Configuration +// ---------------------- + +// Define the base URL for the API const API_BASE_URL = `${removeTrailingSlash(process.env.NEXT_PUBLIC_API_URL || '')}/api/v2`; + +// Retrieve the API token from environment variables const API_TOKEN = process.env.NEXT_PUBLIC_API_TOKEN || ''; -// Axios instance to include any necessary headers -const apiClient = axios.create({ +// Create an Axios instance with default configurations +const apiClient: AxiosInstance = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }); -// Function to subscribe a user to newsletter -export const subscribeToNewsletter = async (body: any) => { +// ---------------------- +// Generic Request Handlers +// ---------------------- + +/** + * Generic GET request handler. + */ +const getRequest = async ( + endpoint: string, + params?: any, +): Promise => { try { - const response = await apiClient.post('/users/newsletter/subscribe', body); + const response: AxiosResponse = await apiClient.get(endpoint, { + params, + }); return response.data; } catch (error) { - console.error(error); + handleError(error, `GET ${endpoint}`); return null; } }; -// Function to post user feedback / inquiry / contact us -export const postContactUs = async (body: any) => { +/** + * Generic POST request handler. + */ +const postRequest = async ( + endpoint: string, + body: any, +): Promise => { try { - const response = await apiClient.post('/users/inquiries/register', body); + const response: AxiosResponse = await apiClient.post(endpoint, body); return response.data; } catch (error) { - console.error(error); + handleError(error, `POST ${endpoint}`); return null; } }; -export const getMaintenances = async (): Promise => { - try { - const response = await apiClient.get('/users/maintenances/website'); - - return response.data; - } catch (error) { - console.error('Error fetching maintenance data:', error); - return null; +/** + * Error handling function. + */ +const handleError = (error: unknown, context: string) => { + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + console.error(`Error in ${context}:`, axiosError.message); + if (axiosError.response) { + console.error('Response data:', axiosError.response.data); + console.error('Response status:', axiosError.response.status); + } + } else { + console.error(`Unexpected error in ${context}:`, error); } }; -// Get grids summary endpoint it uses a api_token to authenticate the request +/** + * Subscribe a user to the newsletter. + */ +export const subscribeToNewsletter = async (body: any): Promise => { + return postRequest('/users/newsletter/subscribe', body); +}; + +/** + * Post user feedback, inquiry, or contact us message. + */ +export const postContactUs = async (body: any): Promise => { + return postRequest('/users/inquiries/register', body); +}; + +/** + * Fetch maintenance data for the website. + */ +export const getMaintenances = async (): Promise => { + return getRequest('/users/maintenances/website'); +}; + +// ---------------------- +// Device Services +// ---------------------- + +/** + * Fetch grids summary data. Requires API token for authentication. + */ export const getGridsSummary = async (): Promise => { try { - const response = await apiClient.get('/devices/grids/summary', { - params: { - token: API_TOKEN, + const response: AxiosResponse = await apiClient.get( + '/devices/grids/summary', + { + params: { + token: API_TOKEN, + }, }, - }); - + ); return response.data; } catch (error) { - console.error('Error fetching grids summary data:', error); + handleError(error, 'GET /devices/grids/summary'); return null; } }; diff --git a/website2/src/views/about/AboutPage.tsx b/website2/src/views/about/AboutPage.tsx index e0826a40ed..db2874a372 100644 --- a/website2/src/views/about/AboutPage.tsx +++ b/website2/src/views/about/AboutPage.tsx @@ -35,11 +35,11 @@ const SkeletonCard: React.FC = () => ( /** Main AboutPage Component **/ const AboutPage: React.FC = () => { - const { boardMembers, isLoading: loadingBoard } = useBoardMembers(); - const { teamMembers, isLoading: loadingTeam } = useTeamMembers(); - const { externalTeamMembers, isLoading: loadingExternalTeam } = + const { data: boardMembers, isLoading: loadingBoard } = useBoardMembers(); + const { data: teamMembers, isLoading: loadingTeam } = useTeamMembers(); + const { data: externalTeamMembers, isLoading: loadingExternalTeam } = useExternalTeamMembers(); - const { partners } = usePartners(); + const { data: partners } = usePartners(); // filter out partners whose website_category is airqo const filteredPartners = partners.filter( diff --git a/website2/src/views/careers/CareerPage.tsx b/website2/src/views/careers/CareerPage.tsx index 6be2218ea1..e24a828bea 100644 --- a/website2/src/views/careers/CareerPage.tsx +++ b/website2/src/views/careers/CareerPage.tsx @@ -12,12 +12,12 @@ import { useCareers, useDepartments } from '@/hooks/useApiHooks'; const CareerPage: React.FC = () => { const router = useRouter(); const { - departments, + data: departments, isLoading: departmentsLoading, isError: departmentsError, } = useDepartments(); const { - careers, + data: careers, isLoading: careersLoading, isError: careersError, } = useCareers(); diff --git a/website2/src/views/careers/DetailsPage.tsx b/website2/src/views/careers/DetailsPage.tsx index 23a9911fac..cdbe4cef50 100644 --- a/website2/src/views/careers/DetailsPage.tsx +++ b/website2/src/views/careers/DetailsPage.tsx @@ -10,7 +10,7 @@ import mainConfig from '@/configs/mainConfigs'; import { useCareerDetails } from '@/hooks/useApiHooks'; const DetailsPage: React.FC<{ id: string }> = ({ id }) => { - const { careerDetails, isLoading, isError } = useCareerDetails(id); + const { data: careerDetails, isLoading, isError } = useCareerDetails(id); const router = useRouter(); if (isLoading) { diff --git a/website2/src/views/cleanairforum/events/EventsPage.tsx b/website2/src/views/cleanairforum/events/EventsPage.tsx index 21ec9f5ae0..b0b0302d78 100644 --- a/website2/src/views/cleanairforum/events/EventsPage.tsx +++ b/website2/src/views/cleanairforum/events/EventsPage.tsx @@ -46,7 +46,7 @@ const categories = [ ]; const EventsPage: React.FC = () => { - const { cleanAirEvents, isLoading, isError } = useCleanAirEvents(); + const { data: cleanAirEvents, isLoading, isError } = useCleanAirEvents(); const [selectedMonth, setSelectedMonth] = useState(null); const [selectedCategory, setSelectedCategory] = useState(null); const [showUpcoming, setShowUpcoming] = useState(true); diff --git a/website2/src/views/cleanairforum/membership/MemberPage.tsx b/website2/src/views/cleanairforum/membership/MemberPage.tsx index 5ce0248185..277bfb2a6e 100644 --- a/website2/src/views/cleanairforum/membership/MemberPage.tsx +++ b/website2/src/views/cleanairforum/membership/MemberPage.tsx @@ -35,7 +35,7 @@ const SkeletonPaginatedSection: React.FC = () => { }; const MemberPage: React.FC = () => { - const { partners, isLoading } = usePartners(); + const { data: partners, isLoading } = usePartners(); // Categorize partners using useMemo for performance optimization const { diff --git a/website2/src/views/cleanairforum/resources/ResourcePage.tsx b/website2/src/views/cleanairforum/resources/ResourcePage.tsx index 7cb4d62ced..28344b55d9 100644 --- a/website2/src/views/cleanairforum/resources/ResourcePage.tsx +++ b/website2/src/views/cleanairforum/resources/ResourcePage.tsx @@ -41,7 +41,11 @@ const LoadingSkeleton = ({ itemsPerPage }: { itemsPerPage: number }) => ( ); const ResourcePage: React.FC = () => { - const { cleanAirResources, isLoading, isError } = useCleanAirResources(); + const { + data: cleanAirResources, + isLoading, + isError, + } = useCleanAirResources(); const itemsPerPage = 3; const { diff --git a/website2/src/views/events/EventPage.tsx b/website2/src/views/events/EventPage.tsx index d241a11d16..d051faef4f 100644 --- a/website2/src/views/events/EventPage.tsx +++ b/website2/src/views/events/EventPage.tsx @@ -12,7 +12,7 @@ import EventCardsSection from '@/views/events/EventCardsSection'; const EventPage: React.FC = () => { const router = useRouter(); - const { airQoEvents, isLoading, isError } = useAirQoEvents(); + const { data: airQoEvents, isLoading, isError } = useAirQoEvents(); const [selectedTab, setSelectedTab] = useState('upcoming'); const upcomingEvents = airQoEvents.filter( diff --git a/website2/src/views/events/SingleEvent.tsx b/website2/src/views/events/SingleEvent.tsx index 632662a8cb..9775c4663f 100644 --- a/website2/src/views/events/SingleEvent.tsx +++ b/website2/src/views/events/SingleEvent.tsx @@ -17,7 +17,7 @@ import { convertDeltaToHtml } from '@/utils/quillUtils'; const SingleEvent: React.FC<{ id: string }> = ({ id }) => { const router = useRouter(); - const { eventDetails, isLoading, isError } = useEventDetails(id); + const { data: eventDetails, isLoading, isError } = useEventDetails(id); // Function to format the date range based on whether the months are the same const formatDateRange = (startDate: string, endDate: string) => { diff --git a/website2/src/views/home/Accordion.tsx b/website2/src/views/home/Accordion.tsx new file mode 100644 index 0000000000..8e86f16e56 --- /dev/null +++ b/website2/src/views/home/Accordion.tsx @@ -0,0 +1,48 @@ +import type React from 'react'; +import { useState } from 'react'; + +type AccordionItem = { + title: string; + content: string; +}; + +type AccordionProps = { + items: AccordionItem[]; +}; + +export const Accordion: React.FC = ({ items }) => { + const [openItem, setOpenItem] = useState(0); + + const toggleItem = (index: number) => { + setOpenItem((prev) => (prev === index ? null : index)); + }; + + return ( +
+ {items.map((item, index) => ( +
+ +
+ {openItem === index && ( +
{item.content}
+ )} +
+
+ ))} +
+ ); +}; diff --git a/website2/src/views/home/FeaturedCarousel.tsx b/website2/src/views/home/FeaturedCarousel.tsx index faf9dd641c..14227eaa15 100644 --- a/website2/src/views/home/FeaturedCarousel.tsx +++ b/website2/src/views/home/FeaturedCarousel.tsx @@ -1,3 +1,5 @@ +'use client'; + import Image from 'next/image'; import Link from 'next/link'; import { useState } from 'react'; @@ -7,7 +9,7 @@ import mainConfig from '@/configs/mainConfigs'; import { useHighlights } from '@/hooks/useApiHooks'; const FeaturedCarousel = () => { - const { highlights, isLoading, isError } = useHighlights(); + const { data: highlights, isLoading } = useHighlights(); const [currentIndex, setCurrentIndex] = useState(0); const nextSlide = () => { @@ -42,18 +44,8 @@ const FeaturedCarousel = () => { ); } - if (isError) { - return ( -
-

- Failed to load highlights. Please try again later. -

-
- ); - } - if (!highlights || highlights.length === 0) { - return null; // Do not render if there are no highlights + return null; } return ( @@ -74,7 +66,7 @@ const FeaturedCarousel = () => {
{item.title} import('react-player/lazy'), { ssr: false }); +// Dynamically import ReactPlayer with forwardRef +const ReactPlayer = dynamic( + () => + import('react-player/lazy').then((mod) => { + const PlayerWithRef = React.forwardRef((props, ref) => ( + + )); + PlayerWithRef.displayName = 'ReactPlayer'; + return PlayerWithRef; + }), + { ssr: false }, +); + +ReactPlayer.displayName = 'ReactPlayer'; const animations = { backdrop: { @@ -28,169 +40,212 @@ const animations = { }, }; -const HomePlayerSection = () => { +interface VideoState { + isModalOpen: boolean; + isBackgroundVideoPlaying: boolean; +} + +const TextSection: React.FC<{ + onExploreData: () => void; + onGetInvolved: () => void; +}> = React.memo(({ onExploreData, onGetInvolved }) => ( +
+

+ Clean air for all African cities +

+

+ + "9 out of 10 people breathe polluted air" + +
+ We empower communities with accurate, hyperlocal and timely air quality + data to drive air pollution mitigation actions. +

+
+ Explore data + + Get involved + +
+
+)); + +TextSection.displayName = 'TextSection'; + +const VideoSection: React.FC<{ + videoRef: React.RefObject; + onPlay: () => void; +}> = React.memo(({ videoRef, onPlay }) => ( +
+
+
+
+)); + +VideoSection.displayName = 'VideoSection'; + +const VideoModal: React.FC<{ + isOpen: boolean; + onClose: () => void; + playerRef: React.RefObject; +}> = React.memo(({ isOpen, onClose, playerRef }) => ( + + e.stopPropagation()} + > + + +
+ +
+ + + Close + +
+
+)); + +VideoModal.displayName = 'VideoModal'; + +const HomePlayerSection: React.FC = () => { const router = useRouter(); const dispatch = useDispatch(); - const [isModalOpen, setIsModalOpen] = useState(false); - const [isBackgroundVideoPlaying, setIsBackgroundVideoPlaying] = - useState(true); + const [videoState, setVideoState] = useState({ + isModalOpen: false, + isBackgroundVideoPlaying: true, + }); const backgroundVideoRef = useRef(null); const modalPlayerRef = useRef(null); - // Handle background video state when modal opens/closes - useEffect(() => { - if (backgroundVideoRef.current) { - if (isModalOpen) { - backgroundVideoRef.current.pause(); - setIsBackgroundVideoPlaying(false); - } else { - // Only play if it was previously playing - if (isBackgroundVideoPlaying) { - backgroundVideoRef.current.play(); - } - } - } - }, [isBackgroundVideoPlaying, isModalOpen]); + const handlePlayButtonClick = useCallback(() => { + setVideoState((prev) => ({ ...prev, isModalOpen: true })); + }, []); - const handlePlayButtonClick = () => { - setIsModalOpen(true); - }; - - const handleCloseModal = () => { - // Stop modal video playback + const handleCloseModal = useCallback(() => { if (modalPlayerRef.current) { modalPlayerRef.current.seekTo(0); } - setIsModalOpen(false); - }; + setVideoState((prev) => ({ ...prev, isModalOpen: false })); + }, []); + + useEffect(() => { + const bgVideo = backgroundVideoRef.current; + if (bgVideo) { + if (videoState.isModalOpen) { + bgVideo.pause(); + } else if (videoState.isBackgroundVideoPlaying) { + bgVideo.play(); + } + } + }, [videoState.isModalOpen, videoState.isBackgroundVideoPlaying]); + + const handleExploreData = useCallback( + () => router.push('/explore-data'), + [router], + ); + const handleGetInvolved = useCallback( + () => dispatch(openModal()), + [dispatch], + ); return (
- {/* Text Section */} -
-

- Clean air for all African cities -

-

- - "9 out of 10 people breathe polluted air" - -
- We empower communities with accurate, hyperlocal and timely air - quality data to drive air pollution mitigation actions. -

-
- router.push('/explore-data')}> - Explore data - - dispatch(openModal())} - className="bg-blue-50 text-blue-600" - > - Get involved - -
-
- - {/* Video Section */} -
-
- {/* Background Video */} -
-
- - {/* Video Modal */} + + - {isModalOpen && ( - - e.stopPropagation()} - > - {/* Close Button */} - - - {/* Modal Video Player */} -
- -
- - - Close - -
-
+ {videoState.isModalOpen && ( + )}
); }; +HomePlayerSection.displayName = 'HomePlayerSection'; + export default HomePlayerSection; diff --git a/website2/src/views/home/HomeStatsSection.tsx b/website2/src/views/home/HomeStatsSection.tsx index b9be4dc630..747febcc68 100644 --- a/website2/src/views/home/HomeStatsSection.tsx +++ b/website2/src/views/home/HomeStatsSection.tsx @@ -1,294 +1,150 @@ 'use client'; -import Enabel from '@public/assets/images/partners/enabel.svg'; -import Google from '@public/assets/images/partners/google.svg'; -import UN from '@public/assets/images/partners/UN.svg'; -import UsMission from '@public/assets/images/partners/usmissionuganda.svg'; -import WorldBank from '@public/assets/images/partners/worldbankgroup.svg'; -import Community from '@public/assets/svgs/ImpactNumbers/Community.svg'; -import Monitor from '@public/assets/svgs/ImpactNumbers/Monitor.svg'; -import Network from '@public/assets/svgs/ImpactNumbers/Network.svg'; -import Partners from '@public/assets/svgs/ImpactNumbers/Partners.svg'; -import Publications from '@public/assets/svgs/ImpactNumbers/Publications.svg'; -import Records from '@public/assets/svgs/ImpactNumbers/Records.svg'; + import Image from 'next/image'; -import React, { useState } from 'react'; +import type React from 'react'; +import { useState } from 'react'; +import { CustomButton } from '@/components/ui'; import mainConfig from '@/configs/mainConfigs'; import { useImpactNumbers } from '@/hooks/useApiHooks'; -import { CustomButton } from '../../components/ui'; - -type AccordionItem = { - title: string; - content: string; -}; - -type AccordionProps = { - items: AccordionItem[]; -}; - -const Accordion: React.FC = ({ items }) => { - const [openItem, setOpenItem] = useState(0); - - const toggleItem = (index: number) => { - setOpenItem((prev) => (prev === index ? null : index)); - }; - - return ( -
- {items.map((item, index) => ( -
- -
- {openItem === index && ( -
{item.content}
- )} -
-
- ))} -
- ); -}; +import { Accordion } from './Accordion'; +import { accordionItems, partnerLogos, statItems } from './data'; const HomeStatsSection: React.FC = () => { - const { impactNumbers, isLoading, isError } = useImpactNumbers(); + const { data: impactNumbers } = useImpactNumbers(); const [activeTab, setActiveTab] = useState<'cities' | 'communities'>( 'cities', ); - const accordionItems: { [key: string]: AccordionItem[] } = { - cities: [ - { - title: 'High Resolution Network', - content: - 'We want cleaner air in all African cities. We leverage our understanding of the African context.', - }, - { - title: 'Digital air quality platforms', - content: - 'We empower decision-makers in African cities We increase access to air quality data evidence.', - }, - { - title: 'Policy engagement', - content: - 'We engage city authorities and government agencies We empower local leaders with air quality information.', - }, - ], - communities: [ - { - title: 'AirQommunity Champions', - content: - 'A growing network of individual change makers Championing local leaders and demand action.', - }, - { - title: 'Free Access To Air Quality Information', - content: - 'We train individuals and communities Facilitating access to air quality information.', - }, - { - title: 'AirQo Hosts', - content: - 'We engage locals host our deployment activities We involve locals in our maintenance drives.', - }, - ], - }; + return ( +
+
+ + + +
+ +
+ ); +}; - const skeletonLoader = ( -
- {Array(6) - .fill(0) - .map((_, index) => ( +const PartnerLogosSection: React.FC = () => ( +
+
+

+ AIRQO IS SUPPORTED BY +

+
+ {partnerLogos.map((partner, index) => (
-
-
-
-
-
+ {`Partner
))} +
- ); - - if (isError) { - return ( -
- Something went wrong +
+); + +const HeadingSection: React.FC<{ + activeTab: 'cities' | 'communities'; + setActiveTab: (tab: 'cities' | 'communities') => void; +}> = ({ activeTab, setActiveTab }) => ( +
+

+ Closing the air quality
data gaps in Africa +

+

+ We provide accurate, hyperlocal, and timely air quality data to provide{' '} +
+ evidence of the magnitude and scale of air pollution across Africa. +

+
+ setActiveTab('cities')} + className={`px-6 py-3 ${ + activeTab === 'cities' + ? 'bg-[#2E3A59] text-white z-10 scale-105 rounded-xl' + : 'bg-[#DFE8F9] text-[#2E3A59] -ml-1 rounded-l-xl' + } border border-[#DFE8F9]`} + > + For African cities + + setActiveTab('communities')} + className={`px-6 py-3 ${ + activeTab === 'communities' + ? 'bg-[#2E3A59] text-white z-10 scale-105 rounded-xl' + : 'bg-[#DFE8F9] text-[#2E3A59] -ml-1 rounded-r-xl' + } border border-[#DFE8F9]`} + > + For Communities + +
+
+); + +const AccordionAndImageSection: React.FC<{ + activeTab: 'cities' | 'communities'; +}> = ({ activeTab }) => ( +
+
+ +
+
+
+ Air quality monitor installation
- ); - } - - return ( -
-
- {/* Partner Logos Section */} -
-
-

- AIRQO IS SUPPORTED BY -

-
- {[Google, UsMission, Enabel, WorldBank, UN].map( - (partner, index) => ( -
- {`Partner -
- ), - )} -
-
-
- - {/* Heading and Subheading */} -
-

- Closing the air quality
data gaps in Africa -

-

- We provide accurate, hyperlocal, and timely air quality data to - provide
- evidence of the magnitude and scale of air pollution across Africa. +

+
+); + +const StatisticsSection: React.FC<{ impactNumbers: any }> = ({ + impactNumbers, +}) => ( +
+ {statItems.map((stat, index) => ( +
+
+

+ {impactNumbers?.[stat.key] ?? 0}+

-
- {/* For African Cities Button */} - setActiveTab('cities')} - className={`px-6 py-3 ${ - activeTab === 'cities' - ? 'bg-[#2E3A59] text-white z-10 scale-105 rounded-xl' - : 'bg-[#DFE8F9] text-[#2E3A59] -ml-1 rounded-l-xl' - } border border-[#DFE8F9]`} - > - For African cities - - - {/* For Communities Button */} - setActiveTab('communities')} - className={`px-6 py-3 ${ - activeTab === 'communities' - ? 'bg-[#2E3A59] text-white z-10 scale-105 rounded-xl' - : 'bg-[#DFE8F9] text-[#2E3A59] -ml-1 rounded-r-xl' - } border border-[#DFE8F9]`} - > - For Communities - -
+

{stat.label}

- - {/* Accordion and Image Section */} -
- {/* Accordion Section */} -
- -
- - {/* Image Section */} -
-
- Air quality monitor installation -
-
+
+ {stat.label}
- - {/* Statistics Section */} - {isLoading ? ( - skeletonLoader - ) : ( -
- {[ - { - label: 'African Cities', - value: `${impactNumbers.african_cities}+`, - icon: Network, - }, - { - label: 'Community Champions', - value: `${impactNumbers.champions}+`, - icon: Community, - }, - { - label: 'Monitor Installations', - value: `${impactNumbers.deployed_monitors}+`, - icon: Monitor, - }, - { - label: 'Data records', - value: `${impactNumbers.data_records}M+`, - icon: Records, - }, - { - label: 'Research papers', - value: `${impactNumbers.research_papers}+`, - icon: Publications, - }, - { - label: 'Partners', - value: `${impactNumbers.partners}+`, - icon: Partners, - }, - ].map((stat, index) => ( -
-
-

{stat.value}

-

{stat.label}

-
-
- {stat.label} -
-
- ))} -
- )} -
- ); -}; + ))} +
+); export default HomeStatsSection; diff --git a/website2/src/views/home/data.ts b/website2/src/views/home/data.ts new file mode 100644 index 0000000000..f4b9e14657 --- /dev/null +++ b/website2/src/views/home/data.ts @@ -0,0 +1,83 @@ +import Enabel from '@public/assets/images/partners/enabel.svg'; +import Google from '@public/assets/images/partners/google.svg'; +import UN from '@public/assets/images/partners/UN.svg'; +import UsMission from '@public/assets/images/partners/usmissionuganda.svg'; +import WorldBank from '@public/assets/images/partners/worldbankgroup.svg'; +import Community from '@public/assets/svgs/ImpactNumbers/Community.svg'; +import Monitor from '@public/assets/svgs/ImpactNumbers/Monitor.svg'; +import Network from '@public/assets/svgs/ImpactNumbers/Network.svg'; +import Partners from '@public/assets/svgs/ImpactNumbers/Partners.svg'; +import Publications from '@public/assets/svgs/ImpactNumbers/Publications.svg'; +import Records from '@public/assets/svgs/ImpactNumbers/Records.svg'; + +export const partnerLogos = [Google, UsMission, Enabel, WorldBank, UN]; + +export const accordionItems = { + cities: [ + { + title: 'High Resolution Network', + content: + 'We want cleaner air in all African cities. We leverage our understanding of the African context.', + }, + { + title: 'Digital air quality platforms', + content: + 'We empower decision-makers in African cities We increase access to air quality data evidence.', + }, + { + title: 'Policy engagement', + content: + 'We engage city authorities and government agencies We empower local leaders with air quality information.', + }, + ], + communities: [ + { + title: 'AirQommunity Champions', + content: + 'A growing network of individual change makers Championing local leaders and demand action.', + }, + { + title: 'Free Access To Air Quality Information', + content: + 'We train individuals and communities Facilitating access to air quality information.', + }, + { + title: 'AirQo Hosts', + content: + 'We engage locals host our deployment activities We involve locals in our maintenance drives.', + }, + ], +}; + +export const statItems = [ + { + label: 'African Cities', + key: 'african_cities', + icon: Network, + }, + { + label: 'Community Champions', + key: 'champions', + icon: Community, + }, + { + label: 'Monitor Installations', + key: 'deployed_monitors', + icon: Monitor, + }, + { + label: 'Data records', + key: 'data_records', + icon: Records, + }, + { + label: 'Research papers', + key: 'research_papers', + icon: Publications, + }, + { + label: 'Partners', + key: 'partners', + icon: Partners, + }, +]; diff --git a/website2/src/views/press/PressPage.tsx b/website2/src/views/press/PressPage.tsx index 1afa2bb16a..3091a6a5fa 100644 --- a/website2/src/views/press/PressPage.tsx +++ b/website2/src/views/press/PressPage.tsx @@ -9,7 +9,7 @@ import mainConfig from '@/configs/mainConfigs'; import { usePressArticles } from '@/hooks/useApiHooks'; const PressPage: React.FC = () => { - const { pressArticles, isLoading, isError } = usePressArticles(); + const { data: pressArticles, isLoading, isError } = usePressArticles(); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 4; diff --git a/website2/src/views/publications/ResourcePage.tsx b/website2/src/views/publications/ResourcePage.tsx index d2d44aa8f0..b377c7d98a 100644 --- a/website2/src/views/publications/ResourcePage.tsx +++ b/website2/src/views/publications/ResourcePage.tsx @@ -9,7 +9,7 @@ import { usePublications } from '@/hooks/useApiHooks'; const ResourcePage: React.FC = () => { const router = useRouter(); - const { publications, isLoading, isError } = usePublications(); + const { data: publications, isLoading, isError } = usePublications(); const searchParams = useSearchParams(); // Tabs mapped to categories from the Publication model @@ -67,7 +67,7 @@ const ResourcePage: React.FC = () => {
{/* Header Section */}
-
+

Resources

Discover our latest collection of resources @@ -98,7 +98,7 @@ const ResourcePage: React.FC = () => {

{/* Resources List Section */} -
+
{isLoading ? ( // Skeleton Loader
diff --git a/website2/src/views/solutions/AfricanCities/AfricanCities.tsx b/website2/src/views/solutions/AfricanCities/AfricanCities.tsx index 693f1381b6..f92476a1b1 100644 --- a/website2/src/views/solutions/AfricanCities/AfricanCities.tsx +++ b/website2/src/views/solutions/AfricanCities/AfricanCities.tsx @@ -5,7 +5,7 @@ import mainConfig from '@/configs/mainConfigs'; import { useAfricanCountries } from '@/hooks/useApiHooks'; const AfricanCities: React.FC = () => { - const { africanCountries, isLoading, isError } = useAfricanCountries(); + const { data: africanCountries } = useAfricanCountries(); const [selectedCountry, setSelectedCountry] = useState(null); const [selectedCity, setSelectedCity] = useState(null); @@ -35,19 +35,7 @@ const AfricanCities: React.FC = () => { setSelectedCity(city); }; - if (isLoading) { - return ( -
Loading African countries...
- ); - } - - if (isError) { - return ( -
- Failed to load data. Please try again later. -
- ); - } + if (!africanCountries) return; return (