diff --git a/src/website2/README.md b/src/website2/README.md index 2cf1206548..62bb2376aa 100644 --- a/src/website2/README.md +++ b/src/website2/README.md @@ -1,38 +1,184 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Website + +Welcome to the Website repository, part of the AirQo Frontend project. This website is built with [Next.js](https://nextjs.org) and was bootstrapped using [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). The live website can be found at [airqo.net](https://airqo.net). + +> **Note:** This repository only contains the frontend portion of the project. The backend has been built with Django and is maintained in the [airqo-api](https://github.com/airqo-platform/airqo-api) repository. If you wish to use the database for the website, please contact the project admin to obtain the necessary database URL for the frontend configuration. + +This guide provides clear, step-by-step instructions to help you set up your local development environment, run the website, and contribute effectively. Additionally, it explains how to handle environment variables and update CI/CD workflows. + +--- + +## Table of Contents + +- [Website](#website) + - [Table of Contents](#table-of-contents) + - [Getting Started](#getting-started) + - [1. Clone the Repository](#1-clone-the-repository) + - [2. Navigate to the Website Folder](#2-navigate-to-the-website-folder) + - [3. Install Dependencies](#3-install-dependencies) + - [4. Run the Development Server](#4-run-the-development-server) + - [Environment Variables \& Workflow Updates](#environment-variables--workflow-updates) + - [Backend \& Database Integration](#backend--database-integration) + - [Contributing](#contributing) + - [Learn More](#learn-more) + - [Deployment](#deployment) + +--- ## Getting Started -First, run the development server: +Follow these steps to set up your local development environment for the website. These instructions are applicable on Windows, Mac, and Linux. + +### 1. Clone the Repository + +Clone the AirQo Frontend repository using the following command: + +```bash +git clone https://github.com/airqo-platform/AirQo-frontend.git +``` + +_Tip for Mac/Linux:_ +If you encounter any permission issues with Git, ensure that Git is correctly installed on your system. You can install Git via [Homebrew](https://brew.sh) on Mac (`brew install git`) or use your package manager on Linux (e.g., `sudo apt-get install git` on Debian/Ubuntu). + +### 2. Navigate to the Website Folder + +After cloning the repository, navigate to the folder that contains the website application: + +```bash +cd AirQo-frontend/src/website2 +``` + +_Note:_ +The folder structure is consistent across all operating systems. Use your terminal (Mac/Linux) or Command Prompt/PowerShell (Windows) to run this command. + +### 3. Install Dependencies + +This project requires **Node.js version 18 or above**. Ensure you have Node.js and npm installed on your system by running: + +```bash +node -v +npm -v +``` + +If you need to install or update Node.js, download it from [nodejs.org](https://nodejs.org/) or use a version manager like [nvm](https://github.com/nvm-sh/nvm) (especially useful for Mac/Linux). + +To install the required dependencies using **npm**, run: + +```bash +npm install +``` + +_For Mac/Linux Users:_ +If you experience permission issues while installing packages globally, consider using a Node version manager like [nvm](https://github.com/nvm-sh/nvm). + +### 4. Run the Development Server + +Launch the development server with the following command: ```bash npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -test +Once the server starts, open [http://localhost:3000](http://localhost:3000) in your browser to view the website. The page will automatically reload as you make changes. + +_Alternate Tips for Mac/Linux:_ + +- Use your terminal’s tab completion to quickly navigate directories. +- If you’re using a firewall or proxy, ensure your local ports are accessible. + +--- + +## Environment Variables & Workflow Updates + +When adding new environment variables to the project, ensure that the CI/CD workflows remain consistent across staging, production, and preview environments. + +For example, if you add new variables, update your workflow YAML files as follows: + +```bash +echo " NEXT_PUBLIC_OPENCAGE_API_KEY: ${{ secrets.WEBSITE_NEXT_PUBLIC_OPENCAGE_API_KEY }}" >> .env.yaml +echo " NEXT_PUBLIC_API_TOKEN: ${{ secrets.WEBSITE_PROD_NEXT_PUBLIC_API_TOKEN }}" >> .env.yaml +``` + +**Important Notes:** + +- Verify that the secret names used in your workflows match those defined in your repository. +- If you add new environment variables, please contact the project admin to have them added to the GitHub secrets. +- Always update the workflow files whenever you introduce or modify environment variables. +- You can find the workflow files for the different environments in the `.github/workflows` folder. + +--- + +## Backend & Database Integration + +The backend for this project is built with Django and is maintained in the [airqo-api](https://github.com/airqo-platform/airqo-api) repository. If you wish to connect the website to a live database: + +- **Database Access:** + You will need the database URL which is not publicly available. + **Action Required:** Contact the project admin to obtain the database URL for frontend integration. + +- **Environment Variables:** + Once you receive the database URL, update your environment variables accordingly in the `.env.yaml` file and any other relevant configuration files. + +--- + +## Contributing + +We welcome contributions from the open source community. To help you get started, please follow these steps: + +1. **Fork and Clone:** + + - Fork the repository on GitHub. + - Clone your fork to your local machine: + ```bash + git clone https://github.com/your-username/AirQo-frontend.git + ``` + +2. **Create a Branch:** + + - Create a feature or bug-fix branch: + ```bash + git checkout -b feature/your-feature-name + ``` + +3. **Make Your Changes:** + + - Follow the coding standards and guidelines established in the project. + - Update documentation as needed. + - Test your changes locally to ensure everything works as expected. + +4. **Commit and Push:** + + - Commit your changes with a clear commit message: + ```bash + git commit -m "Description of your changes" + ``` + - Push your branch to your fork: + ```bash + git push origin feature/your-feature-name + ``` + +5. **Open a Pull Request:** + - Navigate to the main repository on GitHub and open a Pull Request against the `main` branch. + - Provide a detailed description of your changes and reference any related issues. -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +--- -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## Learn More -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +To expand your knowledge about Next.js and modern web development, consider exploring these resources: -## Learn More. +- **[Next.js Documentation](https://nextjs.org/docs)** – Learn about Next.js features and APIs. +- **[Learn Next.js](https://nextjs.org/learn)** – An interactive tutorial for building Next.js applications. +- **[Next.js GitHub Repository](https://github.com/vercel/next.js)** – Explore and contribute to the core Next.js codebase. -To learn more about Next.js, take a look at the following resources: +--- -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +## Deployment -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +The simplest way to deploy the website is via the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme). For more detailed deployment instructions, refer to the [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying). -## Deploy on Vercel +--- -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +Thank you for your interest in contributing to our website. Your support and contributions are vital to the ongoing success and improvement of the project! -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +--- diff --git a/src/website2/src/app/layout.tsx b/src/website2/src/app/layout.tsx index 32a9726c13..2f8b995d6f 100644 --- a/src/website2/src/app/layout.tsx +++ b/src/website2/src/app/layout.tsx @@ -39,7 +39,8 @@ export default async function RootLayout({ children: ReactNode; }) { const maintenance = await checkMaintenance(); - const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || ''; + const GA_MEASUREMENT_ID = + process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || 'G-79ZVCLEDSG'; return ( @@ -47,6 +48,9 @@ export default async function RootLayout({ }> + {/* Initialize & Track Google Analytics only on the client */} + + {maintenance.isActive ? ( ) : ( @@ -58,11 +62,6 @@ export default async function RootLayout({ - - {/* Initialize & Track Google Analytics only on the client */} - {GA_MEASUREMENT_ID && ( - - )} ); diff --git a/src/website2/src/components/GoogleAnalytics.tsx b/src/website2/src/components/GoogleAnalytics.tsx index 0b1d413b1d..fec83d0e60 100644 --- a/src/website2/src/components/GoogleAnalytics.tsx +++ b/src/website2/src/components/GoogleAnalytics.tsx @@ -25,6 +25,8 @@ export default function GoogleAnalytics({ const pathname = usePathname(); const searchParams = useSearchParams(); + console.info('gta', measurementId); + useEffect(() => { if ( typeof window === 'undefined' || diff --git a/src/website2/src/components/sections/footer/CountrySelectorDialog.tsx b/src/website2/src/components/sections/footer/CountrySelectorDialog.tsx index 173a9ead5f..2c7fa9e713 100644 --- a/src/website2/src/components/sections/footer/CountrySelectorDialog.tsx +++ b/src/website2/src/components/sections/footer/CountrySelectorDialog.tsx @@ -40,7 +40,7 @@ const CountrySelectorDialog: React.FC = () => { // Calculate total pages const totalPages = useMemo( () => Math.ceil(airqloudData.length / itemsPerPage), - [airqloudData.length, itemsPerPage], + [airqloudData.length], ); // Cache for flags to avoid redundant fetches @@ -54,9 +54,7 @@ const CountrySelectorDialog: React.FC = () => { } try { const response = await fetch( - `https://restcountries.com/v3.1/name/${encodeURIComponent( - countryName.replace('_', ' '), - )}`, + `https://restcountries.com/v3.1/name/${encodeURIComponent(countryName.replace('_', ' '))}`, { signal: abortSignal }, ); if (!response.ok) { @@ -74,48 +72,36 @@ const CountrySelectorDialog: React.FC = () => { [flagCache], ); - // Fetch airqloud summary using the proxy route + // Fetch airqloud summary using the proxy route or production service const fetchAirqloudSummary = useCallback( async (abortSignal: AbortSignal) => { try { let data; - if (process.env.NODE_ENV === 'development') { - // Use proxy in development const response = await fetch( `/api/proxy?endpoint=devices/grids/summary`, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, signal: abortSignal, }, ); - - if (!response.ok) { - throw new Error(`Error: ${response.statusText}`); - } - + if (!response.ok) throw new Error(`Error: ${response.statusText}`); data = await response.json(); } else { - // Use getGridsSummary in production data = await getGridsSummary(); } - - // Filter the data to only include grids where admin_level is "country" + // Filter data to only include grids where admin_level is "country" const countryLevelData: AirqloudCountry[] = data.grids.filter( (grid: any) => grid.admin_level === 'country', ); - - // Fetch flags dynamically based on long_name + // Dynamically fetch flags const countriesWithFlags = await Promise.all( countryLevelData.map(async (country: AirqloudCountry) => { const flag = await fetchFlag(country.long_name, abortSignal); return { ...country, flag }; }), ); - setAirqloudData(countriesWithFlags); } catch (error) { if ((error as any).name !== 'AbortError') { @@ -126,13 +112,25 @@ const CountrySelectorDialog: React.FC = () => { [fetchFlag], ); - // Function to get the user's country based on their coordinates using a reverse geocoding API + // Helper: Wrap the geolocation API in a promise + const getCurrentPositionAsync = ( + options: PositionOptions = {}, + ): Promise => { + return new Promise((resolve, reject) => { + if (!navigator.geolocation) { + reject(new Error('Geolocation not supported')); + } else { + navigator.geolocation.getCurrentPosition(resolve, reject, options); + } + }); + }; + + // Function to get the user's country based on coordinates using reverse geocoding const fetchUserCountry = useCallback( - async (latitude: number, longitude: number, abortSignal: AbortSignal) => { + async (latitude: number, longitude: number) => { try { const response = await fetch( `https://api.opencagedata.com/geocode/v1/json?q=${latitude}+${longitude}&key=${process.env.NEXT_PUBLIC_OPENCAGE_API_KEY}`, - { signal: abortSignal }, ); if (!response.ok) { throw new Error( @@ -143,79 +141,68 @@ const CountrySelectorDialog: React.FC = () => { const country = data.results[0]?.components?.country || null; setUserCountry(country); } catch (error) { - if ((error as any).name !== 'AbortError') { - console.error('Error fetching user country:', error); - } + console.error('Error fetching user country:', error); setUserCountry(null); } }, [], ); - // Helper function to format long country names and remove underscores + // Helper to format and truncate long country names const formatCountryName = useCallback((name: string) => { - const cleanName = name.replace(/_/g, ' '); // Replace all underscores with spaces - return cleanName.length > 20 ? `${cleanName.slice(0, 20)}...` : cleanName; // Truncate long names + const cleanName = name.replace(/_/g, ' '); + return cleanName.length > 20 ? `${cleanName.slice(0, 20)}...` : cleanName; }, []); - // Effect to get the user's location when the component mounts + // Effect: Get the user's coordinates and fetch the corresponding country useEffect(() => { - const controller = new AbortController(); - const { signal } = controller; - - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition( - (position) => { + let isMounted = true; + (async () => { + try { + const position = await getCurrentPositionAsync({ timeout: 10000 }); + if (isMounted) { const { latitude, longitude } = position.coords; - fetchUserCountry(latitude, longitude, signal); - }, - (error) => { - console.error('Error getting location:', error); + fetchUserCountry(latitude, longitude); + } + } catch (error) { + console.error('Error getting location:', error); + if (isMounted) { setUserCountry(null); - }, - ); - } - + } + } + })(); return () => { - controller.abort(); + isMounted = false; }; }, [fetchUserCountry]); - // Effect to fetch the airqloud data + // Effect: Fetch airqloud data useEffect(() => { const controller = new AbortController(); fetchAirqloudSummary(controller.signal); - return () => { controller.abort(); }; }, [fetchAirqloudSummary]); - // Effect to set the default selected country once data has been loaded + // Effect: Set the default selected country once data is loaded useEffect(() => { if (airqloudData.length > 0) { let defaultCountry: AirqloudCountry | null = null; - - // Check if the user's country is in the airqloudData if (userCountry) { - const foundCountry = airqloudData.find( - (country) => - country.long_name.toLowerCase() === userCountry.toLowerCase(), - ); - defaultCountry = foundCountry || null; + defaultCountry = + airqloudData.find( + (country) => + country.long_name.toLowerCase() === userCountry.toLowerCase(), + ) || null; } - - // If the user's country is not found, fall back to Uganda if (!defaultCountry) { defaultCountry = airqloudData.find( (country) => country.long_name.toLowerCase() === 'uganda', ) || null; } - - // If Uganda is not found, fall back to the first country defaultCountry = defaultCountry || airqloudData[0]; - setSelectedCountryData(defaultCountry); dispatch(setSelectedCountry(defaultCountry)); } @@ -228,11 +215,11 @@ const CountrySelectorDialog: React.FC = () => { setIsOpen(false); }, [dispatch, selectedCountryData]); - // Pagination logic to show only the current page's countries + // Pagination logic for displaying countries on the current page const paginatedCountries = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return airqloudData.slice(startIndex, startIndex + itemsPerPage); - }, [airqloudData, currentPage, itemsPerPage]); + }, [airqloudData, currentPage]); return ( @@ -275,7 +262,7 @@ const CountrySelectorDialog: React.FC = () => { {airqloudData.length > 0 ? ( <>

Selected Country

-
+
{selectedCountryData?.flag && ( { className="rounded-md" /> )} - {/* Tooltip for truncated country names */} {formatCountryName(country.long_name)} @@ -321,7 +307,6 @@ const CountrySelectorDialog: React.FC = () => { ))}
- {/* Pagination component */} {totalPages > 1 && (
{ )} ) : ( - // Friendly interface when no data is available

No countries available at the moment.