By the end of this lesson, you will:
💡 Tip Parallel Power Instead of checking items one at a time (slow), we'll check multiple items at once (fast)! This is like asking several friends the same question at once instead of calling them one by one.
// Slow way - Sequential (one after another)
const pokemon1 = await getPokemon('pikachu'); // Wait 1 second
const pokemon2 = await getPokemon('charizard'); // Wait 1 second
const pokemon3 = await getPokemon('blastoise'); // Wait 1 second
// Total: 3 seconds
// Fast way - Parallel (all at once)
const [pokemon1, pokemon2, pokemon3] = await Promise.all([
getPokemon('pikachu'), // All start together
getPokemon('charizard'), // and finish together
getPokemon('blastoise')
]);
// Total: 1 second!
async function getMultiplePokemon(pokemonNames) {
const BASE_URL = 'https://pokeapi.co/api/v2/pokemon';
try {
console.log('🔥 Fetching Pokemon data...');
// Create array of fetch promises
const pokemonPromises = pokemonNames.map(name =>
fetch(`${BASE_URL}/${name.toLowerCase()}`)
.then(response => {
if (!response.ok) {
throw new Error(`Pokemon ${name} not found`);
}
return response.json();
})
);
// Wait for all to complete
const pokemonData = await Promise.all(pokemonPromises);
console.log('✅ All Pokemon data received!');
return pokemonData;
} catch (error) {
console.error('❌ Failed to fetch Pokemon:', error);
throw error;
}
}
// Usage
const pokemon = ['pikachu', 'charizard', 'blastoise'];
const allPokemon = await getMultiplePokemon(pokemon);
async function getStarWarsCharacters(characterIds) {
const BASE_URL = 'https://swapi.dev/api/people';
// Use Promise.allSettled to handle individual failures
const results = await Promise.allSettled(
characterIds.map(async (id) => {
const response = await fetch(`${BASE_URL}/${id}/`);
if (!response.ok) {
throw new Error(`Failed to get character ${id}`);
}
const data = await response.json();
return { id, data, success: true };
})
);
// Process results
const successful = [];
const failed = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successful.push(result.value);
} else {
failed.push({
id: characterIds[index],
error: result.reason.message,
success: false
});
}
});
return { successful, failed };
}
<!DOCTYPE html>
<html>
<head>
<title>Multiple Items Comparison Dashboard</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f6fa;
}
.comparison-header {
text-align: center;
margin-bottom: 30px;
}
.item-input {
display: flex;
gap: 10px;
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.item-input input {
flex: 1;
padding: 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 16px;
}
.item-input button {
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s;
}
.item-input button:hover {
background: #0056b3;
}
.comparison-type {
margin: 20px 0;
text-align: center;
}
.type-selector {
display: inline-flex;
gap: 10px;
background: white;
padding: 5px;
border-radius: 25px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.type-selector button {
padding: 10px 20px;
border: none;
background: transparent;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s;
}
.type-selector button.active {
background: #007bff;
color: white;
}
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.item-card {
background: white;
border-radius: 15px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
position: relative;
transition: transform 0.3s;
}
.item-card:hover {
transform: translateY(-5px);
}
.item-card.pokemon {
background: linear-gradient(135deg, #ffcb05, #3d7dca);
color: white;
}
.item-card.starwars {
background: linear-gradient(135deg, #000000, #ffe81f);
color: white;
}
.item-card.country {
background: linear-gradient(135deg, #74b9ff, #0984e3);
color: white;
}
.item-card.error {
background: linear-gradient(135deg, #ff7675, #d63031);
color: white;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
}
.item-name {
font-size: 1.4em;
font-weight: bold;
margin: 0;
}
.item-image {
width: 80px;
height: 80px;
border-radius: 10px;
background: rgba(255,255,255,0.2);
display: flex;
align-items: center;
justify-content: center;
margin: 10px 0;
}
.item-stats {
display: grid;
gap: 8px;
}
.stat-item {
display: flex;
justify-content: space-between;
padding: 5px 0;
border-bottom: 1px solid rgba(255,255,255,0.2);
}
.remove-btn {
background: rgba(255,255,255,0.2);
border: none;
color: white;
border-radius: 50%;
width: 30px;
height: 30px;
cursor: pointer;
font-size: 18px;
transition: background 0.3s;
}
.remove-btn:hover {
background: rgba(255,255,255,0.4);
}
.loading {
text-align: center;
padding: 40px;
color: #666;
font-size: 18px;
}
.comparison-stats {
background: white;
border-radius: 15px;
padding: 25px;
margin: 20px 0;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.stat-highlight {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
text-align: center;
border-left: 4px solid #007bff;
}
.stat-value {
font-size: 1.5em;
font-weight: bold;
color: #007bff;
}
.error-message {
text-align: center;
padding: 10px;
background: rgba(255,255,255,0.2);
border-radius: 5px;
margin: 10px 0;
}
@media (max-width: 768px) {
.items-grid {
grid-template-columns: 1fr;
}
.item-input {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="comparison-header">
<h1>🚀 Multiple Items Comparison Dashboard</h1>
<p>Compare Pokemon, Star Wars characters, countries, and more!</p>
</div>
<div class="comparison-type">
<div class="type-selector">
<button onclick="switchType('pokemon')" class="active" id="pokemon-btn">🔥 Pokemon</button>
<button onclick="switchType('starwars')" id="starwars-btn">⭐ Star Wars</button>
<button onclick="switchType('countries')" id="countries-btn">🌍 Countries</button>
</div>
</div>
<div class="item-input">
<input type="text" id="itemInput" placeholder="Enter item name...">
<button onclick="addItem()">Add Item</button>
<button onclick="refreshAll()">Refresh All</button>
</div>
<div id="loading" class="loading" style="display: none;">
🔄 Loading data...
</div>
<div id="itemsGrid" class="items-grid">
<!-- Item cards will be inserted here -->
</div>
<div id="comparisonStats" class="comparison-stats" style="display: none;">
<h3>📊 Comparison Analysis</h3>
<div id="statsContent" class="stats-grid"></div>
</div>
</body>
</html>
class ItemComparison {
constructor() {
this.currentType = 'pokemon';
this.items = {
pokemon: ['pikachu', 'charizard', 'blastoise'],
starwars: ['1', '2', '3'], // Character IDs
countries: ['japan', 'france', 'brazil']
};
this.itemData = [];
this.init();
}
async init() {
await this.loadAllItems();
this.setupEventListeners();
}
setupEventListeners() {
// Enter key to add item
document.getElementById('itemInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.addItem();
}
});
}
async loadAllItems() {
const currentItems = this.items[this.currentType];
if (currentItems.length === 0) return;
this.showLoading(true);
try {
console.log(`🚀 Loading ${this.currentType} data for ${currentItems.length} items...`);
// Create promises for all items
const itemPromises = currentItems.map(item => this.fetchItemData(item));
// Wait for all to complete
const results = await Promise.allSettled(itemPromises);
// Process results
this.itemData = [];
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
this.itemData.push(result.value);
} else {
console.error(`Failed to load ${currentItems[index]}:`, result.reason);
this.itemData.push({
name: currentItems[index],
error: result.reason.message,
success: false
});
}
});
this.renderItems();
this.updateComparisonStats();
} catch (error) {
console.error('Failed to load items:', error);
} finally {
this.showLoading(false);
}
}
async fetchItemData(item) {
switch (this.currentType) {
case 'pokemon':
return await this.fetchPokemon(item);
case 'starwars':
return await this.fetchStarWarsCharacter(item);
case 'countries':
return await this.fetchCountry(item);
default:
throw new Error('Unknown type');
}
}
async fetchPokemon(name) {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`);
if (!response.ok) {
throw new Error(`Pokemon ${name} not found`);
}
const data = await response.json();
return {
name: data.name,
id: data.id,
types: data.types.map(t => t.type.name),
stats: {
hp: data.stats.find(s => s.stat.name === 'hp').base_stat,
attack: data.stats.find(s => s.stat.name === 'attack').base_stat,
defense: data.stats.find(s => s.stat.name === 'defense').base_stat,
speed: data.stats.find(s => s.stat.name === 'speed').base_stat
},
sprite: data.sprites.front_default,
success: true,
type: 'pokemon'
};
}
async fetchStarWarsCharacter(id) {
const response = await fetch(`https://swapi.dev/api/people/${id}/`);
if (!response.ok) {
throw new Error(`Character ${id} not found`);
}
const data = await response.json();
return {
name: data.name,
height: data.height,
mass: data.mass,
birthYear: data.birth_year,
eyeColor: data.eye_color,
hairColor: data.hair_color,
success: true,
type: 'starwars'
};
}
async fetchCountry(name) {
const response = await fetch(`https://restcountries.com/v3.1/name/${name}`);
if (!response.ok) {
throw new Error(`Country ${name} not found`);
}
const data = await response.json();
const country = data[0];
return {
name: country.name.common,
capital: country.capital?.[0] || 'N/A',
population: country.population,
area: country.area,
region: country.region,
flag: country.flag,
success: true,
type: 'countries'
};
}
renderItems() {
const grid = document.getElementById('itemsGrid');
grid.innerHTML = this.itemData.map((itemData, index) => {
if (!itemData.success) {
return this.renderErrorCard(itemData, index);
}
switch (itemData.type) {
case 'pokemon':
return this.renderPokemonCard(itemData, index);
case 'starwars':
return this.renderStarWarsCard(itemData, index);
case 'countries':
return this.renderCountryCard(itemData, index);
default:
return '';
}
}).join('');
}
renderPokemonCard(pokemon, index) {
return `
<div class="item-card pokemon">
<div class="item-header">
<h3 class="item-name">${pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1)}</h3>
<button class="remove-btn" onclick="itemApp.removeItem(${index})">×</button>
</div>
<div class="item-image">
${pokemon.sprite ? `<img src="${pokemon.sprite}" alt="${pokemon.name}" style="width: 60px; height: 60px;">` : '🔥'}
</div>
<div class="item-stats">
<div class="stat-item">
<span>Types:</span>
<span>${pokemon.types.join(', ')}</span>
</div>
<div class="stat-item">
<span>⚔️ Attack:</span>
<span>${pokemon.stats.attack}</span>
</div>
<div class="stat-item">
<span>🛡️ Defense:</span>
<span>${pokemon.stats.defense}</span>
</div>
<div class="stat-item">
<span>❤️ HP:</span>
<span>${pokemon.stats.hp}</span>
</div>
<div class="stat-item">
<span>💨 Speed:</span>
<span>${pokemon.stats.speed}</span>
</div>
</div>
</div>
`;
}
renderStarWarsCard(character, index) {
return `
<div class="item-card starwars">
<div class="item-header">
<h3 class="item-name">${character.name}</h3>
<button class="remove-btn" onclick="itemApp.removeItem(${index})">×</button>
</div>
<div class="item-image">⭐</div>
<div class="item-stats">
<div class="stat-item">
<span>📏 Height:</span>
<span>${character.height} cm</span>
</div>
<div class="stat-item">
<span>⚖️ Mass:</span>
<span>${character.mass} kg</span>
</div>
<div class="stat-item">
<span>🎂 Birth Year:</span>
<span>${character.birthYear}</span>
</div>
<div class="stat-item">
<span>👁️ Eyes:</span>
<span>${character.eyeColor}</span>
</div>
<div class="stat-item">
<span>💇 Hair:</span>
<span>${character.hairColor}</span>
</div>
</div>
</div>
`;
}
renderCountryCard(country, index) {
return `
<div class="item-card country">
<div class="item-header">
<h3 class="item-name">${country.name}</h3>
<button class="remove-btn" onclick="itemApp.removeItem(${index})">×</button>
</div>
<div class="item-image">${country.flag}</div>
<div class="item-stats">
<div class="stat-item">
<span>🏛️ Capital:</span>
<span>${country.capital}</span>
</div>
<div class="stat-item">
<span>👥 Population:</span>
<span>${(country.population / 1000000).toFixed(1)}M</span>
</div>
<div class="stat-item">
<span>📏 Area:</span>
<span>${country.area.toLocaleString()} km²</span>
</div>
<div class="stat-item">
<span>🌍 Region:</span>
<span>${country.region}</span>
</div>
</div>
</div>
`;
}
renderErrorCard(data, index) {
return `
<div class="item-card error">
<div class="item-header">
<h3 class="item-name">${data.name}</h3>
<button class="remove-btn" onclick="itemApp.removeItem(${index})">×</button>
</div>
<div class="error-message">
<div>❌ Error loading data</div>
<div>${data.error}</div>
<button onclick="itemApp.retryItem(${index})" style="margin-top: 10px; padding: 5px 10px; background: rgba(255,255,255,0.2); border: none; color: white; border-radius: 5px; cursor: pointer;">
🔄 Retry
</button>
</div>
</div>
`;
}
updateComparisonStats() {
const successfulItems = this.itemData.filter(item => item.success);
if (successfulItems.length < 2) {
document.getElementById('comparisonStats').style.display = 'none';
return;
}
let statsHTML = '';
switch (this.currentType) {
case 'pokemon':
statsHTML = this.generatePokemonStats(successfulItems);
break;
case 'starwars':
statsHTML = this.generateStarWarsStats(successfulItems);
break;
case 'countries':
statsHTML = this.generateCountryStats(successfulItems);
break;
}
document.getElementById('statsContent').innerHTML = statsHTML;
document.getElementById('comparisonStats').style.display = 'block';
}
generatePokemonStats(pokemon) {
const attacks = pokemon.map(p => p.stats.attack);
const defenses = pokemon.map(p => p.stats.defense);
const speeds = pokemon.map(p => p.stats.speed);
const strongest = pokemon[attacks.indexOf(Math.max(...attacks))];
const fastest = pokemon[speeds.indexOf(Math.max(...speeds))];
const tankiest = pokemon[defenses.indexOf(Math.max(...defenses))];
return `
<div class="stat-highlight">
<div>⚔️ Strongest Attacker</div>
<div class="stat-value">${strongest.name}</div>
<div>${Math.max(...attacks)} Attack</div>
</div>
<div class="stat-highlight">
<div>💨 Fastest</div>
<div class="stat-value">${fastest.name}</div>
<div>${Math.max(...speeds)} Speed</div>
</div>
<div class="stat-highlight">
<div>🛡️ Best Defense</div>
<div class="stat-value">${tankiest.name}</div>
<div>${Math.max(...defenses)} Defense</div>
</div>
<div class="stat-highlight">
<div>🏆 Team Size</div>
<div class="stat-value">${pokemon.length}</div>
<div>Pokemon</div>
</div>
`;
}
generateStarWarsStats(characters) {
const heights = characters.map(c => parseInt(c.height) || 0);
const masses = characters.map(c => parseInt(c.mass) || 0);
const tallest = characters[heights.indexOf(Math.max(...heights))];
const heaviest = characters[masses.indexOf(Math.max(...masses))];
return `
<div class="stat-highlight">
<div>📏 Tallest</div>
<div class="stat-value">${tallest.name}</div>
<div>${tallest.height} cm</div>
</div>
<div class="stat-highlight">
<div>⚖️ Heaviest</div>
<div class="stat-value">${heaviest.name}</div>
<div>${heaviest.mass} kg</div>
</div>
<div class="stat-highlight">
<div>👥 Characters</div>
<div class="stat-value">${characters.length}</div>
<div>Total</div>
</div>
`;
}
generateCountryStats(countries) {
const populations = countries.map(c => c.population);
const areas = countries.map(c => c.area);
const mostPopulous = countries[populations.indexOf(Math.max(...populations))];
const largest = countries[areas.indexOf(Math.max(...areas))];
const totalPop = populations.reduce((a, b) => a + b, 0);
return `
<div class="stat-highlight">
<div>👥 Most Populous</div>
<div class="stat-value">${mostPopulous.name}</div>
<div>${(mostPopulous.population / 1000000).toFixed(1)}M people</div>
</div>
<div class="stat-highlight">
<div>📏 Largest</div>
<div class="stat-value">${largest.name}</div>
<div>${largest.area.toLocaleString()} km²</div>
</div>
<div class="stat-highlight">
<div>🌍 Total Population</div>
<div class="stat-value">${(totalPop / 1000000).toFixed(1)}M</div>
<div>Combined</div>
</div>
`;
}
async switchType(newType) {
this.currentType = newType;
// Update button states
document.querySelectorAll('.type-selector button').forEach(btn => {
btn.classList.remove('active');
});
document.getElementById(`${newType}-btn`).classList.add('active');
// Update placeholder
const placeholders = {
pokemon: 'Enter Pokemon name (e.g. pikachu)',
starwars: 'Enter character ID (e.g. 4 for Darth Vader)',
countries: 'Enter country name (e.g. canada)'
};
document.getElementById('itemInput').placeholder = placeholders[newType];
await this.loadAllItems();
}
async addItem() {
const input = document.getElementById('itemInput');
const itemName = input.value.trim();
if (!itemName) return;
// Check if item already exists
if (this.items[this.currentType].some(item =>
item.toLowerCase() === itemName.toLowerCase())) {
alert('Item already added!');
return;
}
this.items[this.currentType].push(itemName);
input.value = '';
// Load just the new item
this.showLoading(true);
try {
const newItemData = await this.fetchItemData(itemName);
this.itemData.push(newItemData);
} catch (error) {
this.itemData.push({
name: itemName,
error: error.message,
success: false
});
}
this.renderItems();
this.updateComparisonStats();
this.showLoading(false);
}
removeItem(index) {
this.items[this.currentType].splice(index, 1);
this.itemData.splice(index, 1);
this.renderItems();
this.updateComparisonStats();
}
async retryItem(index) {
const itemName = this.items[this.currentType][index];
this.showLoading(true);
try {
const itemData = await this.fetchItemData(itemName);
this.itemData[index] = itemData;
} catch (error) {
this.itemData[index] = {
name: itemName,
error: error.message,
success: false
};
}
this.renderItems();
this.updateComparisonStats();
this.showLoading(false);
}
async refreshAll() {
await this.loadAllItems();
}
showLoading(show) {
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
}
// Initialize the app
const itemApp = new ItemComparison();
// Make functions available globally
window.switchType = (type) => itemApp.switchType(type);
window.addItem = () => itemApp.addItem();
window.refreshAll = () => itemApp.refreshAll();
class APIManager {
constructor() {
this.apis = {
pokemon: {
baseUrl: 'https://pokeapi.co/api/v2/pokemon',
transformer: this.transformPokemon
},
starwars: {
baseUrl: 'https://swapi.dev/api/people',
transformer: this.transformStarWars
},
countries: {
baseUrl: 'https://restcountries.com/v3.1/name',
transformer: this.transformCountry
}
};
}
async fetchMultiple(type, items) {
const api = this.apis[type];
if (!api) throw new Error(`Unknown API type: ${type}`);
const promises = items.map(item =>
this.fetchSingle(api, item)
.then(data => api.transformer(data))
.catch(error => ({ error: error.message, success: false }))
);
return await Promise.allSettled(promises);
}
async fetchSingle(api, item) {
const response = await fetch(`${api.baseUrl}/${item}`);
if (!response.ok) {
throw new Error(`Failed to fetch ${item}`);
}
return await response.json();
}
transformPokemon(data) {
return {
name: data.name,
stats: data.stats.reduce((acc, stat) => {
acc[stat.stat.name] = stat.base_stat;
return acc;
}, {}),
types: data.types.map(t => t.type.name),
sprite: data.sprites.front_default,
success: true
};
}
transformStarWars(data) {
return {
name: data.name,
height: data.height,
mass: data.mass,
birthYear: data.birth_year,
success: true
};
}
transformCountry(data) {
const country = Array.isArray(data) ? data[0] : data;
return {
name: country.name.common,
population: country.population,
area: country.area,
region: country.region,
success: true
};
}
}
class ComparisonEngine {
constructor() {
this.comparators = {
numeric: (a, b) => b - a, // Descending
string: (a, b) => a.localeCompare(b) // Ascending
};
}
findBest(items, field, type = 'numeric') {
if (items.length === 0) return null;
const values = items.map(item => this.getNestedValue(item, field));
const comparator = this.comparators[type];
const sortedValues = [...values].sort(comparator);
const bestValue = sortedValues[0];
return items.find(item =>
this.getNestedValue(item, field) === bestValue
);
}
getNestedValue(obj, path) {
return path.split('.').reduce((current, key) =>
current && current[key] !== undefined ? current[key] : null, obj
);
}
calculateStats(items, fields) {
const stats = {};
fields.forEach(field => {
const values = items
.map(item => this.getNestedValue(item, field))
.filter(val => val !== null && !isNaN(val))
.map(Number);
if (values.length > 0) {
stats[field] = {
min: Math.min(...values),
max: Math.max(...values),
avg: values.reduce((a, b) => a + b, 0) / values.length,
total: values.reduce((a, b) => a + b, 0)
};
}
});
return stats;
}
}
Add support for comparing movies, books, or sports teams using other free APIs.
Add filters to show only items that meet certain criteria (e.g., Pokemon above certain stats).
Add a button to export comparison results as JSON or CSV.
You've learned to:
Parallel requests and flexible data handling make your comparisons fast and versatile!
Great job mastering multiple items comparison! You can now efficiently compare any type of data from different APIs. In our next lesson, we'll explore API Rate Limiting & Optimization - how to work within API limits and make your requests even more efficient.