diff --git a/CountriesData.json b/CountriesData.json index a6fc2a2..3db67ea 100644 --- a/CountriesData.json +++ b/CountriesData.json @@ -1,174 +1,203 @@ [ { "name": "Åland Islands", + "flag": "https://flagcdn.com/w320/ax.png", "population": 21225, "region": "Europe", "capital": "Mariehamn" }, { "name": "Afghanistan", + "flag": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5c/Flag_of_the_Taliban.svg/320px-Flag_of_the_Taliban.svg.png", "population": 27657145, "region": "Asia", "capital": "Kabul" }, { "name": "Albania", + "flag": "https://flagcdn.com/w320/al.png", "population": 2886026, "region": "Europe", "capital": "Tirana" }, { "name": "Algeria", + "flag": "https://flagcdn.com/w320/dz.png", "population": 40400000, "region": "Africa", "capital": "Algiers" }, { "name": "American Samoa", + "flag": "https://flagcdn.com/w320/as.png", "population": 57100, "region": "Oceania", "capital": "Pago Pago" }, { "name": "Andorra", + "flag": "https://flagcdn.com/w320/ad.png", "population": 78014, "region": "Europe", "capital": "Andorra la Vella" }, { "name": "Angola", + "flag": "https://flagcdn.com/w320/ao.png", "population": 25868000, "region": "Africa", "capital": "Luanda" }, { "name": "Anguilla", + "flag": "https://flagcdn.com/w320/ai.png", "population": 13452, "region": "Americas", "capital": "The Valley" }, { "name": "Antarctica", + "flag": "https://flagcdn.com/w320/aq.png", "population": 1000, "region": "Polar", "capital": "" }, { "name": "Israel", + "flag": "https://flagcdn.com/w320/il.png", "population": 8527400, "region": "Asia", "capital": "Jerusalem" }, { "name": "Italy", + "flag": "https://flagcdn.com/w320/it.png", "population": 60665551, "region": "Europe", "capital": "Rome" }, { "name": "Jamaica", + "flag": "https://flagcdn.com/w320/jm.png", "population": 2723246, "region": "Americas", "capital": "Kingston" }, { "name": "Japan", + "flag": "https://flagcdn.com/w320/jp.png", "population": 126960000, "region": "Asia", "capital": "Tokyo" }, { "name": "Jersey", + "flag": "https://flagcdn.com/w320/je.png", "population": 100800, "region": "Europe", "capital": "Saint Helier" }, { "name": "Jordan", + "flag": "https://flagcdn.com/w320/jo.png", "population": 9531712, "region": "Asia", "capital": "Amman" }, { "name": "Kazakhstan", + "flag": "https://flagcdn.com/w320/kz.png", "population": 17753200, "region": "Asia", "capital": "Astana" }, { "name": "Kenya", + "flag": "https://flagcdn.com/w320/ke.png", "population": 47251000, "region": "Africa", "capital": "Nairobi" }, { "name": "Kiribati", + "flag": "https://flagcdn.com/w320/ki.png", "population": 113400, "region": "Oceania", "capital": "South Tarawa" }, { "name": "Kuwait", + "flag": "https://flagcdn.com/w320/kw.png", "population": 4183658, "region": "Asia", "capital": "Kuwait City" }, { "name": "Kyrgyzstan", + "flag": "https://flagcdn.com/w320/kg.png", "population": 6047800, "region": "Asia", "capital": "Bishkek" }, { "name": "Laos", + "flag": "https://flagcdn.com/w320/la.png", "population": 6492400, "region": "Asia", "capital": "Vientiane" }, { "name": "Latvia", + "flag": "https://flagcdn.com/w320/lv.png", "population": 1961600, "region": "Europe", "capital": "Riga" }, { "name": "Lebanon", + "flag": "https://flagcdn.com/w320/lb.png", "population": 5988000, "region": "Asia", "capital": "Beirut" }, { "name": "Lesotho", + "flag": "https://flagcdn.com/w320/ls.png", "population": 1894194, "region": "Africa", "capital": "Maseru" }, { "name": "Liberia", + "flag": "https://flagcdn.com/w320/lr.png", "population": 4615000, "region": "Africa", "capital": "Monrovia" }, { "name": "Libya", + "flag": "https://flagcdn.com/w320/ly.png", "population": 6385000, "region": "Africa", "capital": "Tripoli" }, { "name": "Liechtenstein", + "flag": "https://flagcdn.com/w320/li.png", "population": 37623, "region": "Europe", "capital": "Vaduz" }, { "name": "Lithuania", + "flag": "https://flagcdn.com/w320/lt.png", "population": 2872294, "region": "Europe", "capital": "Vilnius" }, { "name": "Luxembourg", + "flag": "https://flagcdn.com/w320/lu.png", "population": 576200, "region": "Europe", "capital": "Luxembourg" diff --git a/css/common.css b/css/common.css index 7d63db5..a6ad536 100644 --- a/css/common.css +++ b/css/common.css @@ -307,3 +307,24 @@ body.dark-theme .country-info li { body.dark-theme .country-info strong { color: white; } + +.toast { + position: fixed; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + background-color: #444; + color: #fff; + padding: 8px 15px; + border-radius: 4px; + font-size: 14px; + display: none; /* Initially hidden */ + z-index: 1000; /* Ensure it's above other elements */ + opacity: 0; /* Start invisible */ + transition: opacity 0.3s ease-in-out; /* Smooth fade in/out */ +} + +.toast.visible { + display: block; /* Show the toast */ + opacity: 1; /* Make it fully visible */ +} diff --git a/details.html b/details.html index b584581..fafc7ef 100644 --- a/details.html +++ b/details.html @@ -12,15 +12,9 @@ - + - + @@ -43,7 +37,7 @@ -
+
-
-
+
+
- - + + + diff --git a/index.html b/index.html index f4afbf5..540cf41 100644 --- a/index.html +++ b/index.html @@ -88,71 +88,10 @@

Where in the world?

+ + diff --git a/js/common.js b/js/common.js new file mode 100644 index 0000000..a3c704f --- /dev/null +++ b/js/common.js @@ -0,0 +1,174 @@ +const jsonFilePath = './CountriesData.json'; + +/** + * Retrieves the country name from the specified query parameter in the URL. + * @param {string} param - The name of the query parameter containing the country name. + * @returns {string|null} - The country name, or null if the parameter doesn't exist. + */ +const getQueryParameter = (param) => { + const params = new URLSearchParams(window.location.search); + return params.get(param); +}; + +/** + * Removes all child elements from a given parent container. + * + * This function iterates through all child nodes of the specified parent container + * and removes them one by one using the `removeChild` method. It ensures that the + * parent container is emptied of all its children. + * + * @param {HTMLElement} parentContainer - The parent container whose child elements are to be removed. + * If the parent container is null or undefined, the function does nothing. + */ +function clearAllChildrenFromParent(parentContainer) { + if (!parentContainer) return; + + while (parentContainer.firstChild) { + const firstChildElement = parentContainer.firstChild; // Get the first child element + parentContainer.removeChild(firstChildElement); // Remove the first child element explicitly + } +} + + +/** + * Fetches and parses JSON data from a given file path. + * @param {string} filePath - The path to the JSON file or API endpoint. + * @returns {Promise} - A promise that resolves to the parsed JSON data. + * @throws {Error} - Throws an error if the fetch request fails or if the response is not OK. + */ +async function fetchData(filePath) { + try { + const response = await fetch(filePath); + if (!response.ok) { + throw new Error(`Failed to fetch JSON: ${response.statusText}`); + } + return await response.json(); // Returns the parsed JSON data + } catch (error) { + console.error('Error fetching data:', error); + throw error; // Rethrows the error for external handling + } +} + +/** + * Creates an HTML element representing a country. + * This function dynamically generates an element for a country, + * either for a grid view or a detailed view, based on the provided options. + * @param {Object} country - The country data object containing details like name, population, region, capital, and flag URL. + * @param {Object} [options] - Configuration object to determine the rendering mode. + * @param {boolean} [options.isGrid=true] - Determines whether the country should be rendered in grid view (default) or detailed view. + * @returns {HTMLElement} - The generated HTML element for the country. + */ +function createCountryElement(country, options = {isGrid: true}) { + // Set the container element for the flag and information section + const container = document.createElement(options.isGrid ? 'a' : 'section'); + + // Assign the main class to the container based on its type + container.className = options.isGrid ? 'country scale-effect' : 'country-details'; + + // Add a link if this is a grid page + if (options.isGrid) { + container.href = `details.html?name=${encodeURIComponent(country.name)}`; + } + + // Create the flag area - shared for both types + const flagDiv = document.createElement('div'); + flagDiv.className = 'country-flag'; + + // Create an image inside the flag container + const flagImg = document.createElement('img'); + flagImg.src = country.flag; + flagImg.alt = `${country.name} Flag`; + flagDiv.appendChild(flagImg); // Append the image to the flag container + + // Create the information section + const infoDiv = document.createElement('div'); + infoDiv.className = 'country-info'; + + const title = document.createElement('h1'); + title.textContent = country.name; + + const population = document.createElement('p'); + const populationLabel = document.createElement('strong'); + populationLabel.textContent = 'Population: '; + population.appendChild(populationLabel); // Add the bold label + population.appendChild(document.createTextNode(country.population)); // Add the population text + + const region = document.createElement('p'); + const regionLabel = document.createElement('strong'); + regionLabel.textContent = 'Region: '; + region.appendChild(regionLabel); // Add the bold label + region.appendChild(document.createTextNode(country.region)); // Add the region text + + const capital = document.createElement('p'); + const capitalLabel = document.createElement('strong'); + capitalLabel.textContent = 'Capital: '; + capital.appendChild(capitalLabel); // Add the bold label + capital.appendChild(document.createTextNode(country.capital)); // Add the capital text + + infoDiv.append(title, population, region, capital); // Append details to the information container + container.append(flagDiv, infoDiv); // Append the flag area and information section to the main container + + return container; +} + + +/** + * Handles the theme toggle functionality: + * - Applies the saved theme (light or dark) from localStorage when the page loads. + * - Toggles the theme between light and dark when the toggle button is clicked. + * - Saves the user's theme preference in localStorage to persist it across pages. + */ +document.addEventListener('DOMContentLoaded', () => { + const body = document.body; + const toggleButton = document.querySelector('.theme-toggle'); + const themeText = toggleButton.querySelector('.theme-text'); + const THEME_DARK = 'dark'; + const THEME_LIGHT = 'light'; + + // Retrieve the saved theme from localStorage + const savedTheme = localStorage.getItem('theme'); + if (savedTheme === THEME_DARK) { + // Apply dark theme if previously saved as dark + body.classList.add('dark-theme'); + themeText.textContent = 'Light Mode'; // Update toggle text to reflect current theme + } else { + // Default to light theme + themeText.textContent = 'Dark Mode'; + } + + // Toggle theme on button click + toggleButton.addEventListener('click', () => { + const isDark = body.classList.toggle('dark-theme'); // Toggle dark theme class + localStorage.setItem('theme', isDark ? THEME_DARK : THEME_LIGHT); + themeText.textContent = isDark ? 'Light Mode' : 'Dark Mode'; // Update toggle text + }); +}); + + + + + + +/** + * Displays a toast notification with the given message. + * If the toast element doesn't exist, it is created dynamically. + * @param {string} message - The message to display in the toast. + */ +function showToast(message) { + let toast = document.getElementById('toast'); + + if (!toast) { + toast = document.createElement('div'); + toast.id = 'toast'; + toast.className = 'toast'; + document.body.appendChild(toast); + } + + // Set the message and make the toast visible + toast.textContent = message; + toast.classList.add('visible'); + + setTimeout(() => { + toast.classList.remove('visible'); // Remove the visibility class + }, 1000); +} \ No newline at end of file diff --git a/js/details.js b/js/details.js new file mode 100644 index 0000000..529c535 --- /dev/null +++ b/js/details.js @@ -0,0 +1,41 @@ +/** + * - Fetches and displays details for a specific country based on the URL parameter. + * - Retrieves the country name from the URL. + * - Fetches country data and finds the relevant country. + * - Displays the country's details or logs an error if not found. + * - Hides the loader after completing the process. + */ +document.addEventListener('DOMContentLoaded', async () => { + const countryDetailsSection = document.getElementById('country-details-id'); + const loader = document.querySelector('.loader'); + const countryName = getQueryParameter('name'); + + if (!countryName) { + hideLoader(loader); + return console.error('No country name in URL'); + } + + try { + const countries = await fetchData(jsonFilePath); // Await fetching the data + const country = findCountryByName(countries, countryName); // Find the country + if (!country) throw new Error(`Country with name "${countryName}" not found!`); + + clearAllChildrenFromParent(countryDetailsSection); // Clear previous content + countryDetailsSection.appendChild(createCountryElement(country, { isGrid: false })); // Append new content + } catch (error) { + console.error('Error fetching country details:', error); // Handle errors + } finally { + hideLoader(loader); // Always hide the loader + } +}); + +/** Hides the loader element. */ +function hideLoader(loader) { + if (loader) loader.style.display = 'none'; +} + +/** Finds a country by name in a list of countries. */ +function findCountryByName(countries, name) { + return countries.find((country) => country.name === name); +} + diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..bbf2957 --- /dev/null +++ b/js/index.js @@ -0,0 +1,122 @@ +/** + * Initializes the countries grid by fetching country data and rendering it to the page. + * - Fetches data from a JSON file. + * - Builds and displays the grid of countries. + * - Logs an error if data fetching fails. + */ +document.addEventListener('DOMContentLoaded', async () => { + const gridElement = document.querySelector('.countries-grid'); + + try { + const countries = await fetchData(jsonFilePath); // Await fetching the data + buildCountriesGrid(countries, gridElement, true); // Build the grid with the fetched data + } catch (error) { + console.error('Error initializing grid:', error); // Handle any errors + } +}); + +/** + * Builds a grid displaying a list of countries with their data. + * @param {Object} countries - Array of country objects containing data. + * @param {HTMLElement} targetElement - The HTML element where the grid will be built. + * @param {boolean} isGrid - If true, builds a grid view; otherwise, a single country view. + */ +function buildCountriesGrid(countries, targetElement, isGrid = true) { + clearAllChildrenFromParent(targetElement); + countries.forEach((country) => { + const countryElement = createCountryElement(country, {isGrid}); + targetElement.appendChild(countryElement); + }); +} + +/** + * Initializes dropdown functionality for region filtering. + * - Toggles dropdown open/close on header click. + * - Calls the `updateGrid` function to filter and display the results. + */ +document.addEventListener('DOMContentLoaded', () => { + const dropdownWrapper = document.querySelector('.filters .dropdown-wrapper'); + const dropdownHeader = dropdownWrapper.querySelector('.dropdown-header'); + const dropdownBody = dropdownWrapper.querySelector('.dropdown-body'); + + // Toggles the dropdown open/close state + dropdownHeader.addEventListener('click', () => { + dropdownWrapper.classList.toggle('open'); // Toggle the "open" class + }); + + // Handles region selection and updates the grid + dropdownBody.addEventListener('click', async ({target}) => { + const selectedRegion = target.dataset.region; // Retrieve the selected region + if (!selectedRegion) return; // Exit if no region selected + + await updateGrid(selectedRegion, true); // Update grid by region + dropdownWrapper.classList.remove('open'); // Close the dropdown + }); +}); + + +/** + * Listens to the input of search country search box and updates the grid. + * - Calls the `updateGrid` function to display the results. + * @event input - Triggered whenever the user types in the search box. + */ +document.addEventListener('DOMContentLoaded', () => { + const searchInput = document.querySelector('.search-input'); + + // Handles search input and updates the grid + searchInput.addEventListener('input', async (event) => { + const originalValue = event.target.value; // The input value before filtering + const filteredValue = originalValue.replace(/[^a-zA-Z\s]/g, ''); // Filter input to allow only English letters and spaces + + // If the value was modified (invalid characters were removed), display a toast + if (originalValue !== filteredValue) { + showToast('Please type in English only.'); // Display a toast notification + } + + event.target.value = filteredValue; // Update the input with the filtered value + const dataOfCountryInput = filteredValue.trim(); // Trim whitespace + await updateGrid(dataOfCountryInput, false); // Update the grid based on the filtered input + }); +}); + +/** + * Updates the grid based on the selected region , searched country or its prefix. + * - Fetches country data from the JSON file. + * - Filters countries based on the selected region or search term. + * - Builds and displays the filtered grid. + * + * @param {string} filterValue - The value for filtering (region or prefix). + * @param {boolean} isRegionFilter - True if filtering by region, false for search prefix. + */ +async function updateGrid(filterValue, isRegionFilter) { + const gridElement = document.querySelector('.countries-grid'); + + // Normalize function to handle special characters + const normalize = (str) => str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); + + try { + // Loads all countries from the data source + const countries = await fetchData(jsonFilePath); + + // Filters countries based on the condition + const filteredCountries = countries.filter((country) => { + if (isRegionFilter) { + // Filter by region + return filterValue === 'all' || normalize(country.region) === normalize(filterValue); + } else { + // Filter by prefix + return normalize(country.name).startsWith(normalize(filterValue)); + } + }); + + // Displays the filtered countries in the grid + buildCountriesGrid(filteredCountries, gridElement); + } catch (error) { + console.error('Error updating grid:', error); + } +} + + + + +