Practice and reinforce the concepts from Lesson 5
title: "Activity: API Widget Building" description: "Build interactive widgets using various APIs" duration: "60 minutes" difficulty: "Intermediate" objectives:
By completing this activity, you will:
Access the template here: Widget Builder Template
git clone
and navigate to the template folderindex.html
in your browser to start!Pick one widget that interests you most, or challenge yourself with multiple widgets!
Create a comprehensive weather widget with multiple data points and user interaction.
class WeatherWidget {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.apiKey = 'YOUR_API_KEY_HERE';
this.units = 'metric'; // or 'imperial'
this.init();
}
async init() {
this.render();
this.setupEventListeners();
await this.loadWeatherByLocation();
}
render() {
this.container.innerHTML = `
<div class="weather-widget">
<div class="weather-header">
<h2>Weather Dashboard</h2>
<div class="controls">
<input type="text" id="cityInput" placeholder="Enter city name">
<button id="searchBtn">Search</button>
<button id="locationBtn">📍 My Location</button>
<button id="unitToggle">°C / °F</button>
</div>
</div>
<div id="weatherDisplay">
<p>Loading weather data...</p>
</div>
<div id="forecastDisplay">
<!-- 5-day forecast will appear here -->
</div>
</div>
`;
}
async getCurrentWeather(city) {
const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${this.apiKey}&units=${this.units}`;
try {
const response = await fetch(url);
if (!response.ok) throw new Error('City not found');
const data = await response.json();
this.displayCurrentWeather(data);
return data;
} catch (error) {
this.displayError(error.message);
}
}
displayCurrentWeather(data) {
const tempSymbol = this.units === 'metric' ? '°C' : '°F';
document.getElementById('weatherDisplay').innerHTML = `
<div class="current-weather">
<div class="weather-main">
<h3>${data.name}, ${data.sys.country}</h3>
<div class="temperature">${Math.round(data.main.temp)}${tempSymbol}</div>
<div class="description">${data.weather[0].description}</div>
</div>
<div class="weather-icon">
<img src="https://openweathermap.org/img/wn/${data.weather[0].icon}@2x.png"
alt="${data.weather[0].description}">
</div>
<div class="weather-details">
<div class="detail">
<span>Feels like</span>
<span>${Math.round(data.main.feels_like)}${tempSymbol}</span>
</div>
<div class="detail">
<span>Humidity</span>
<span>${data.main.humidity}%</span>
</div>
<div class="detail">
<span>Wind</span>
<span>${data.wind.speed} ${this.units === 'metric' ? 'm/s' : 'mph'}</span>
</div>
<div class="detail">
<span>Pressure</span>
<span>${data.main.pressure} hPa</span>
</div>
</div>
</div>
`;
}
}
// Initialize the widget
const weatherWidget = new WeatherWidget('weatherContainer');
Create an interactive Pokemon database explorer with search, favorites, and detailed information.
class PokemonWidget {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.baseUrl = 'https://pokeapi.co/api/v2';
this.favorites = JSON.parse(localStorage.getItem('favoritePokemon') || '[]');
this.init();
}
async init() {
this.render();
this.setupEventListeners();
await this.loadRandomPokemon();
}
render() {
this.container.innerHTML = `
<div class="pokemon-widget">
<div class="pokemon-header">
<h2>Pokemon Explorer</h2>
<div class="controls">
<input type="text" id="pokemonInput" placeholder="Pokemon name or ID">
<button id="searchBtn">Search</button>
<button id="randomBtn">🎲 Random</button>
<button id="favoritesBtn">⭐ Favorites (${this.favorites.length})</button>
</div>
</div>
<div id="pokemonDisplay">
<p>Loading Pokemon...</p>
</div>
<div id="comparisonArea" style="display: none;">
<!-- Pokemon comparison will appear here -->
</div>
</div>
`;
}
async getPokemon(identifier) {
try {
const response = await fetch(`${this.baseUrl}/pokemon/${identifier.toLowerCase()}`);
if (!response.ok) throw new Error('Pokemon not found');
const pokemon = await response.json();
// Get additional data
const speciesResponse = await fetch(pokemon.species.url);
const species = await speciesResponse.json();
this.displayPokemon(pokemon, species);
return { pokemon, species };
} catch (error) {
this.displayError(error.message);
}
}
displayPokemon(pokemon, species) {
const isFavorite = this.favorites.includes(pokemon.id);
document.getElementById('pokemonDisplay').innerHTML = `
<div class="pokemon-card">
<div class="pokemon-header">
<h3>${pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1)}</h3>
<button class="favorite-btn ${isFavorite ? 'favorited' : ''}"
onclick="pokemonWidget.toggleFavorite(${pokemon.id})">
${isFavorite ? '⭐' : '☆'}
</button>
</div>
<div class="pokemon-images">
<img src="${pokemon.sprites.front_default}" alt="${pokemon.name} front">
<img src="${pokemon.sprites.back_default}" alt="${pokemon.name} back">
${pokemon.sprites.front_shiny ?
`<img src="${pokemon.sprites.front_shiny}" alt="${pokemon.name} shiny">` : ''}
</div>
<div class="pokemon-info">
<div class="basic-info">
<p><strong>ID:</strong> #${pokemon.id.toString().padStart(3, '0')}</p>
<p><strong>Height:</strong> ${pokemon.height / 10} m</p>
<p><strong>Weight:</strong> ${pokemon.weight / 10} kg</p>
<p><strong>Base Experience:</strong> ${pokemon.base_experience}</p>
</div>
<div class="types">
<h4>Types</h4>
<div class="type-list">
${pokemon.types.map(type =>
`<span class="type type-${type.type.name}">${type.type.name}</span>`
).join('')}
</div>
</div>
<div class="abilities">
<h4>Abilities</h4>
<div class="ability-list">
${pokemon.abilities.map(ability =>
`<span class="ability">${ability.ability.name}</span>`
).join('')}
</div>
</div>
<div class="stats">
<h4>Base Stats</h4>
${pokemon.stats.map(stat =>
`<div class="stat">
<span class="stat-name">${stat.stat.name}:</span>
<span class="stat-value">${stat.base_stat}</span>
<div class="stat-bar">
<div class="stat-fill" style="width: ${(stat.base_stat / 255) * 100}%"></div>
</div>
</div>`
).join('')}
</div>
</div>
<div class="pokemon-actions">
<button onclick="pokemonWidget.addToComparison(${pokemon.id})">
Compare
</button>
<button onclick="pokemonWidget.loadEvolutionChain('${species.evolution_chain.url}')">
Evolution Chain
</button>
</div>
</div>
`;
}
async loadRandomPokemon() {
const randomId = Math.floor(Math.random() * 1010) + 1; // Gen 1-8 Pokemon
await this.getPokemon(randomId);
}
toggleFavorite(pokemonId) {
const index = this.favorites.indexOf(pokemonId);
if (index > -1) {
this.favorites.splice(index, 1);
} else {
this.favorites.push(pokemonId);
}
localStorage.setItem('favoritePokemon', JSON.stringify(this.favorites));
this.updateFavoritesCount();
}
updateFavoritesCount() {
document.getElementById('favoritesBtn').textContent = `⭐ Favorites (${this.favorites.length})`;
}
}
// Initialize the widget
const pokemonWidget = new PokemonWidget('pokemonContainer');
Create an engaging space exploration widget featuring NASA's astronomical data and imagery.
class SpaceWidget {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.apiKey = 'YOUR_NASA_API_KEY_HERE';
this.currentView = 'apod';
this.init();
}
async init() {
this.render();
this.setupEventListeners();
await this.loadAPOD();
}
render() {
this.container.innerHTML = `
<div class="space-widget">
<div class="space-header">
<h2>NASA Space Explorer</h2>
<div class="navigation">
<button class="nav-btn active" data-view="apod">🌌 Picture of the Day</button>
<button class="nav-btn" data-view="mars">🔴 Mars Photos</button>
<button class="nav-btn" data-view="neo">☄️ Near Earth Objects</button>
<button class="nav-btn" data-view="earth">🌍 Earth Images</button>
</div>
</div>
<div id="spaceDisplay">
<p>Loading space data...</p>
</div>
</div>
`;
}
async loadAPOD(date = null) {
const url = date ?
`https://api.nasa.gov/planetary/apod?api_key=${this.apiKey}&date=${date}` :
`https://api.nasa.gov/planetary/apod?api_key=${this.apiKey}`;
try {
const response = await fetch(url);
const data = await response.json();
document.getElementById('spaceDisplay').innerHTML = `
<div class="apod-container">
<div class="apod-controls">
<input type="date" id="apodDate" max="${new Date().toISOString().split('T')[0]}">
<button onclick="spaceWidget.loadAPODByDate()">Load Date</button>
<button onclick="spaceWidget.loadRandomAPOD()">🎲 Random</button>
</div>
<div class="apod-content">
<h3>${data.title}</h3>
<p class="apod-date">${data.date}</p>
${data.media_type === 'video' ?
`<iframe src="${data.url}" frameborder="0" allowfullscreen></iframe>` :
`<img src="${data.url}" alt="${data.title}" class="apod-image">`
}
<div class="apod-explanation">
<h4>Explanation</h4>
<p>${data.explanation}</p>
</div>
${data.copyright ? `<p class="copyright">© ${data.copyright}</p>` : ''}
</div>
</div>
`;
} catch (error) {
this.displayError('Failed to load Astronomy Picture of the Day');
}
}
async loadMarsPhotos() {
try {
const response = await fetch(
`https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?sol=1000&api_key=${this.apiKey}`
);
const data = await response.json();
if (data.photos.length === 0) {
throw new Error('No photos available for this date');
}
const photos = data.photos.slice(0, 12); // Show first 12 photos
document.getElementById('spaceDisplay').innerHTML = `
<div class="mars-container">
<div class="mars-controls">
<h3>Mars Rover Photos - Sol 1000</h3>
<p>Showing ${photos.length} of ${data.photos.length} photos from Curiosity rover</p>
</div>
<div class="mars-gallery">
${photos.map(photo => `
<div class="mars-photo">
<img src="${photo.img_src}" alt="Mars photo ${photo.id}"
onclick="spaceWidget.showLargeImage('${photo.img_src}', '${photo.camera.full_name}')">
<div class="photo-info">
<p><strong>Camera:</strong> ${photo.camera.full_name}</p>
<p><strong>Date:</strong> ${photo.earth_date}</p>
</div>
</div>
`).join('')}
</div>
</div>
`;
} catch (error) {
this.displayError('Failed to load Mars photos');
}
}
async loadNearEarthObjects() {
const today = new Date().toISOString().split('T')[0];
try {
const response = await fetch(
`https://api.nasa.gov/neo/rest/v1/feed?start_date=${today}&end_date=${today}&api_key=${this.apiKey}`
);
const data = await response.json();
const neos = data.near_earth_objects[today] || [];
document.getElementById('spaceDisplay').innerHTML = `
<div class="neo-container">
<h3>Near Earth Objects Today</h3>
<p>Found ${neos.length} objects approaching Earth today</p>
<div class="neo-list">
${neos.slice(0, 10).map(neo => `
<div class="neo-item">
<h4>${neo.name}</h4>
<div class="neo-details">
<p><strong>Estimated Diameter:</strong>
${Math.round(neo.estimated_diameter.meters.estimated_diameter_min)} -
${Math.round(neo.estimated_diameter.meters.estimated_diameter_max)} meters</p>
<p><strong>Relative Velocity:</strong>
${Math.round(neo.close_approach_data[0].relative_velocity.kilometers_per_hour)} km/h</p>
<p><strong>Miss Distance:</strong>
${Math.round(neo.close_approach_data[0].miss_distance.kilometers)} km</p>
<p class="hazardous ${neo.is_potentially_hazardous_asteroid ? 'dangerous' : 'safe'}">
${neo.is_potentially_hazardous_asteroid ? '⚠️ Potentially Hazardous' : '✅ Safe'}
</p>
</div>
</div>
`).join('')}
</div>
</div>
`;
} catch (error) {
this.displayError('Failed to load Near Earth Objects data');
}
}
}
// Initialize the widget
const spaceWidget = new SpaceWidget('spaceContainer');
.weather-widget {
max-width: 600px;
margin: 20px auto;
background: linear-gradient(135deg, #74b9ff, #0984e3);
border-radius: 15px;
padding: 20px;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.current-weather {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
align-items: center;
}
.temperature {
font-size: 3em;
font-weight: bold;
margin: 10px 0;
}
.weather-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 20px;
}
.detail {
display: flex;
justify-content: space-between;
padding: 10px;
background: rgba(255, 255, 255, 0.1);
border-radius: 5px;
}
.pokemon-widget {
max-width: 800px;
margin: 20px auto;
background: #f8f9fa;
border-radius: 15px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.pokemon-card {
background: white;
border-radius: 10px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.pokemon-images {
display: flex;
justify-content: center;
gap: 20px;
margin: 20px 0;
}
.type {
display: inline-block;
padding: 5px 15px;
border-radius: 20px;
color: white;
font-weight: bold;
margin: 2px;
}
.type-fire { background: #ff6b6b; }
.type-water { background: #74c0fc; }
.type-grass { background: #51cf66; }
.type-electric { background: #ffd43b; color: #333; }
.stat-bar {
background: #e9ecef;
border-radius: 10px;
height: 8px;
margin-top: 5px;
overflow: hidden;
}
.stat-fill {
background: linear-gradient(90deg, #51cf66, #38d9a9);
height: 100%;
transition: width 0.3s ease;
}
.space-widget {
max-width: 900px;
margin: 20px auto;
background: linear-gradient(135deg, #1a1a2e, #16213e);
border-radius: 15px;
padding: 20px;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.apod-image {
width: 100%;
max-width: 600px;
border-radius: 10px;
margin: 20px 0;
}
.mars-gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 20px;
}
.mars-photo img {
width: 100%;
border-radius: 8px;
cursor: pointer;
transition: transform 0.3s ease;
}
.mars-photo img:hover {
transform: scale(1.05);
}
.neo-item {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 15px;
margin: 10px 0;
}
.hazardous.dangerous {
color: #ff6b6b;
}
.hazardous.safe {
color: #51cf66;
}
Your widget should demonstrate:
Combine multiple widgets into a comprehensive dashboard:
class DashboardManager {
constructor() {
this.widgets = new Map();
this.activeWidgets = [];
}
registerWidget(name, widget) {
this.widgets.set(name, widget);
}
activateWidget(name, containerId) {
const WidgetClass = this.widgets.get(name);
if (WidgetClass) {
const instance = new WidgetClass(containerId);
this.activeWidgets.push(instance);
return instance;
}
}
deactivateAllWidgets() {
this.activeWidgets.forEach(widget => {
if (widget.destroy) widget.destroy();
});
this.activeWidgets = [];
}
}
// Usage
const dashboard = new DashboardManager();
dashboard.registerWidget('weather', WeatherWidget);
dashboard.registerWidget('pokemon', PokemonWidget);
dashboard.registerWidget('space', SpaceWidget);
// Allow users to choose which widgets to display
dashboard.activateWidget('weather', 'widget1');
dashboard.activateWidget('pokemon', 'widget2');
Add user preferences and customization:
class WidgetPreferences {
constructor(widgetName) {
this.widgetName = widgetName;
this.storageKey = `${widgetName}_preferences`;
this.preferences = this.load();
}
load() {
const stored = localStorage.getItem(this.storageKey);
return stored ? JSON.parse(stored) : this.getDefaults();
}
save() {
localStorage.setItem(this.storageKey, JSON.stringify(this.preferences));
}
get(key) {
return this.preferences[key];
}
set(key, value) {
this.preferences[key] = value;
this.save();
}
getDefaults() {
return {
theme: 'default',
units: 'metric',
autoRefresh: true,
refreshInterval: 300000 // 5 minutes
};
}
}
API Key Problems:
CORS Errors:
Rate Limiting:
Data Display Issues:
Congratulations on building your API widget! You've successfully:
In the next concept, you'll learn how to optimize API performance, implement caching strategies, and handle more complex API authentication methods.
Your widget demonstrates the power of APIs to bring dynamic, real-world data into web applications. Keep experimenting and building! 🚀