“Where in the world?” App: Deep Dive
In this tutorial, we’ll explore a small vanilla‑JavaScript app that:
- Fetches country data from a REST API
- Displays each country as a card (flag + key details)
- Supports search, filter-by-region, pagination
- Toggles between light/dark themes
All code lives in one HTML file with embedded CSS and JS. Let’s break it down.
HTML Structure
<body>
<!-- Header & Theme Toggle -->
<header class="header">
<h1 class="logo">Where in the world?</h1>
<button class="mode-toggle">
<i class="fas fa-moon"></i> Dark Mode
</button>
</header>
<!-- Main Content -->
<main class="main">
<!-- Search & Filter Controls -->
<div class="controls">
<div class="search-container">
<input id="searchInput" placeholder="Search for a country…" />
<i class="fas fa-search search-icon"></i>
</div>
<div class="filter-container">
<select id="regionFilter">
<option value="">Filter by Region</option>
<option value="Africa">Africa</option>
<option value="Americas">Americas</option>
<option value="Asia">Asia</option>
<option value="Europe">Europe</option>
<option value="Oceania">Oceania</option>
</select>
</div>
</div>
<!-- Country Cards Injected Here -->
<section id="cardsContainer" class="cards"></section>
<!-- Pagination Controls -->
<div class="pagination">
<button id="prevBtn">Prev</button>
<span id="pageInfo"></span>
<button id="nextBtn">Next</button>
</div>
</main>
</body>
-
<header>
: Displays title + theme‑toggle button. -
.controls
: Contains search input + region filter dropdown. -
#cardsContainer
: JavaScript injects country cards here. -
.pagination
: “Prev” / page info / “Next”.
CSS Highlights
:root {
--bg-color: hsl(0, 0%, 98%);
--text-color: hsl(200, 15%, 8%);
--element-color: #fff;
}
.dark-mode {
--bg-color: hsl(207, 26%, 17%);
--text-color: #fff;
--element-color: hsl(209, 23%, 22%);
}
body {
background: var(--bg-color);
color: var(--text-color);
}
/* Cards Grid */
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 2rem;
}
/* Country Card */
.card {
background: var(--element-color);
border-radius: 5px;
overflow: hidden;
box-shadow: 0 2px 9px rgba(0,0,0,0.05);
}
.card img {
width: 100%;
height: 150px;
object-fit: cover;
}
.card .info {
padding: 1rem;
}
-
CSS variables toggle theme via adding/removing
.dark-mode
on<body>
. - Responsive grid for cards ensures neat layout across viewports.
JavaScript Walkthrough
const API_URL = "https://restcountries.com/v2/all";
let countries = [];
let filteredList = [];
let currentPage = 1;
const itemsPerPage = 12;
// 1. Fetch & initialize
async function fetchCountries() {
const res = await fetch(API_URL);
countries = await res.json();
filteredList = countries;
renderPage();
}
fetchCountries();
// 2. Render cards for current page
function renderPage() {
const totalPages = Math.ceil(filteredList.length / itemsPerPage);
const start = (currentPage - 1) * itemsPerPage;
const slice = filteredList.slice(start, start + itemsPerPage);
displayCountries(slice);
updatePagination(totalPages);
}
// 3. Display a list of country cards
function displayCountries(list) {
const container = document.getElementById("cardsContainer");
container.innerHTML = "";
list.forEach(c => {
const card = document.createElement("div");
card.className = "card";
card.innerHTML = `
<img src="${c.flag}" alt="Flag of ${c.name}" />
<div class="info">
<h2>${c.name}</h2>
<p><strong>Population:</strong> ${c.population.toLocaleString()}</p>
<p><strong>Region:</strong> ${c.region}</p>
<p><strong>Capital:</strong> ${c.capital || "—"}</p>
</div>`;
container.appendChild(card);
});
}
// 4. Update pagination controls
function updatePagination(totalPages) {
document.getElementById("pageInfo").textContent = `${currentPage} / ${totalPages}`;
document.getElementById("prevBtn").disabled = currentPage === 1;
document.getElementById("nextBtn").disabled = currentPage === totalPages;
}
// 5. Event listeners: Prev / Next
document.getElementById("prevBtn").addEventListener("click", () => {
if (currentPage > 1) currentPage--, renderPage();
});
document.getElementById("nextBtn").addEventListener("click", () => {
const totalPages = Math.ceil(filteredList.length / itemsPerPage);
if (currentPage < totalPages) currentPage++, renderPage();
});
// 6. Search filter
document.getElementById("searchInput").addEventListener("input", e => {
const term = e.target.value.trim().toLowerCase();
filteredList = countries.filter(c => c.name.toLowerCase().includes(term));
currentPage = 1;
renderPage();
});
// 7. Region filter
document.getElementById("regionFilter").addEventListener("change", e => {
const region = e.target.value;
filteredList = region
? countries.filter(c => c.region === region)
: countries;
currentPage = 1;
renderPage();
});
// 8. Dark/light mode toggle
document.querySelector(".mode-toggle").addEventListener("click", function() {
document.body.classList.toggle("dark-mode");
this.innerHTML = document.body.classList.contains("dark-mode")
? `<i class="fas fa-sun"></i> Light Mode`
: `<i class="fas fa-moon"></i> Dark Mode`;
});
- Data Fetch: Load all countries once.
- Pagination Logic: Slice the filtered list into pages of 12.
-
Search & Filter: Update
filteredList
on input/change, reset page to 1. -
Theme Toggle: Flip CSS variables via
.dark-mode
.
Final Thoughts
This compact example demonstrates:
- Vanilla JS for DOM updates & state management
- Responsive design with CSS Grid & custom properties
- Progressive enhancement: users without JS still see the static HTML
Top comments (0)