<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="author" content="Wendi Wang; A01345480">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/globals.css">
<title>PokéWalk</title>
</head>
<body onload="firstLoad()">
<!-- Wendi Wang -->
<!-- A01345480 -->
<div id="filter-controls">
<label for="typeSelect">Filter Pokémon by type:</label>
<select id="typeSelect">
<option value="">Choose your type</option>
</select>
<button id="clear-filters">Clear Filters</button>
</div>
<div id="spinner">
</div>
<h1>The Pokémon Trail</h1>
<p>Discovering Pokémons one step at a time</p>
<div id="my-container" class="data-container">
</div>
<div class="character" style="width: 100%; height: 400px;">
<model-viewer class="walking" src="/walking.glb" alt="A 3D model of a character" camera-controls animation-name="auto" autoplay camera-orbit="-90deg 75deg auto"></model-viewer>
</div>
<!-- load more button -->
<div id="loadMoreBtn" class="btn" style="display: none;" onclick="loadList()">
LOAD MORE
</div>
<script type="module" src="https://unpkg.com/@google/model-viewer"></script>
<script>
// Variables for pagination and loading state.
var lastPage = 0;
var pageSize = 7; // Determines the number of Pokémon to fetch in each request.
var isLoading = false; // Prevents initiating multiple fetch requests simultaneously.
var pokemonCount = 0; // Tracks the number of Pokémon currently displayed.
var targetPokemonTypeCount = 50; // Set the target count for fetched Pokémon by type
// Intersection Observer setup for lazy loading/infinite scrolling.
let observer;
const observerOptions = {
root: null, // Observes elements in the viewport.
threshold: 0.1, // Triggers when 10% of the target is visible.
};
// Initializes the Intersection Observer to load more Pokémon as the user scrolls.
function setupObserver() {
observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadList(); // Calls to load more Pokémon.
observer.unobserve(entry.target); // Stops observing the current target to prevent repeat loads.
}
});
}, observerOptions);
}
// Called when the page first loads to set everything up.
function firstLoad() {
setupObserver(); // Sets up the Intersection Observer.
loadList(); // Loads the first set of Pokémon.
}
// Fetches a list of Pokémon using the PokéAPI.
function loadList() {
if (isLoading) return; // Exits if a load operation is already in progress.
isLoading = true; // Marks the loading state as true.
createSpinnerElements();
showSpinner(); // Displays the loading spinner.
let offset = lastPage * pageSize; // Calculates the offset for pagination.
// Fetches Pokémon data from the API.
fetch(`https://pokeapi.co/api/v2/pokemon?limit=${pageSize}&offset=${offset}`)
.then(response => response.json())
.then(data => {
// Maps over the fetched Pokémon to get detailed data for each.
var promises = data.results.map(pokemon => fetch(pokemon.url).then(resp => resp.json()));
return Promise.all(promises); // Waits for all detailed data to be fetched.
})
.then(detailsArray => {
// Adds each Pokémon's detailed data to the container.
detailsArray.forEach(details => addToContainer(details));
if (detailsArray.length === pageSize) {
lastPage++; // Prepares for the next load operation.
document.querySelector('#loadMoreBtn').style.display = 'none'; // Hides the Load More button if not needed.
}
})
.catch(error => {
console.error('Fetch error:', error); // Logs errors to the console.
})
.finally(() => {
hideSpinner(); // Hides the spinner once loading is complete.
isLoading = false; // Resets the loading state.
});
}
// Adds detailed Pokémon data to the DOM.
function addToContainer(details) {
var myContainer = document.querySelector('#my-container');
var pokemonBox = document.createElement('div');
pokemonBox.classList.add('pokemon-box');
// Displays the Pokémon's name.
var nameElement = document.createElement('h2');
nameElement.innerText = `Name: ${details.name}`;
pokemonBox.appendChild(nameElement);
// Displays the Pokémon's sprite, if available.
if(details.sprites && details.sprites.front_default) {
var spriteElement = document.createElement('img');
spriteElement.src = details.sprites.front_default;
spriteElement.alt = `Sprite of ${details.name}`;
pokemonBox.appendChild(spriteElement);
}
// Lists the Pokémon's types.
var typesElement = document.createElement('p');
typesElement.innerText = 'Types: ' + details.types.map(type => type.type.name).join(', ');
pokemonBox.appendChild(typesElement);
// Lists the Pokémon's abilities.
var abilitiesElement = document.createElement('p');
abilitiesElement.innerText = 'Abilities: ' + details.abilities.map(ability => ability.ability.name).join(', ');
pokemonBox.appendChild(abilitiesElement);
// Displays the Pokémon's height.
var heightElement = document.createElement('p');
heightElement.innerText = `Height: ${details.height}`;
pokemonBox.appendChild(heightElement);
// Displays the Pokémon's weight.
var weightElement = document.createElement('p');
weightElement.innerText = `Weight: ${details.weight}`;
pokemonBox.appendChild(weightElement);
pokemonCount++; // Increments the Pokémon counter for observer logic.
myContainer.appendChild(pokemonBox); // Adds the new Pokémon box to the container.
// Attaches the observer to every 5th Pokémon for infinite scrolling.
if (pokemonCount % 5 === 0) {
observer.observe(pokemonBox);
}
}
// Creates spinner elements for visual indication of loading.
function createSpinnerElements() {
var spinner = document.querySelector('#spinner');
// Checks and adds a 'conical' spinner element if not present.
if (!spinner.querySelector('.conical')) {
var conicalDiv = document.createElement('div');
conicalDiv.className = 'conical';
spinner.appendChild(conicalDiv);
}
// Checks and adds a 'start' spinner element if not present.
if (!spinner.querySelector('.start')) {
var startDiv = document.createElement('div');
startDiv.className = 'start';
spinner.appendChild(startDiv);
}
}
// Shows the loading spinner.
function showSpinner() {
var spinner = document.querySelector('#spinner');
spinner.style.display = 'block';
}
// Hides the loading spinner after ensuring it's shown for a minimum time.
function hideSpinner() {
const minimumSpinnerDisplayTime = 1000; // Ensures spinner visibility for at least 1 second.
setTimeout(() => {
var spinner = document.querySelector('#spinner');
spinner.style.display = 'none';
}, minimumSpinnerDisplayTime);
}
// Scrolls the container horizontally at a set speed.
function scrollContainer() {
const container = document.getElementById('my-container');
let speed = .5; // Sets the speed of the scroll.
function step() {
// Scrolls the container if not fully scrolled; resets to start otherwise.
if (container.scrollWidth - container.clientWidth > container.scrollLeft) {
container.scrollLeft += speed;
requestAnimationFrame(step);
} else {
container.scrollLeft = 0;
requestAnimationFrame(step);
}
}
requestAnimationFrame(step); // Initiates the scrolling animation.
}
scrollContainer(); // Calls the scrollContainer function to start the horizontal scroll.
// Fetches Pokémon by type and updates the container.
function fetchPokemonByType(pokemonType) {
if (isLoading) return; // Prevents multiple concurrent fetches.
isLoading = true;
createSpinnerElements();
showSpinner(); // Displays the loading spinner.
const fetchBatch = (offset) => {
// Fetches Pokémon of a specific type from the PokéAPI.
fetch(`https://pokeapi.co/api/v2/type/${pokemonType}`)
.then(response => response.json())
.then(data => {
// Maps over the returned Pokémon data to fetch details for each.
const pokemonUrls = data.pokemon.slice(offset, offset + targetPokemonTypeCount).map(p => p.pokemon.url);
return Promise.all(pokemonUrls.map(url => fetch(url).then(resp => resp.json())));
})
.then(detailsArray => {
// Adds each Pokémon's details to the container.
detailsArray.forEach(details => addToContainer(details));
pokemonTypeFetched += detailsArray.length; // Updates the count of fetched Pokémon by type.
// Checks if enough Pokémon have been fetched; hides Load More button if so.
if (pokemonTypeFetched < targetPokemonTypeCount) {
fetchBatch(pokemonTypeOffset + targetPokemonTypeCount);
} else {
document.querySelector('#loadMoreBtn').style.display = 'none';
}
})
.catch(error => {
console.error('Fetch error:', error); // Logs fetch errors to the console.
})
.finally(() => {
hideSpinner(); // Hides the spinner once loading is complete.
isLoading = false; // Resets the loading state.
});
};
fetchBatch(pokemonTypeOffset); // Initiates fetching Pokémon by type.
}
// Sets up event listeners for Pokémon type filters.
function setupFilterButton(pokemonType) {
document.getElementById(`filter-${pokemonType}`).addEventListener('click', function() {
resetForTypeFetch(); // Resets the UI for type fetch.
fetchPokemonByType(pokemonType); // Fetches Pokémon of the selected type.
});
}
// Array of Pokémon types for generating filter options.
const pokemonTypes = [
'normal', 'fire', 'water', 'electric', 'grass', 'ice',
'fighting', 'poison', 'ground', 'flying', 'psychic', 'bug',
'rock', 'ghost', 'dragon', 'dark', 'steel', 'fairy'
];
// Populates the type selection dropdown and sets up its change listener.
function setupTypeSelector() {
const selectElement is= document.getElementById('typeSelect');
// Adds each Pokémon type as an option to the dropdown.
pokemonTypes.forEach(type => {
const option = document.createElement('option');
option.value = type;
option.textContent = type.charAt(0).toUpperCase() + type.slice(1); // Capitalizes the first letter of the type.
selectElement.appendChild(option);
});
// Handles type selection to fetch and display Pokémon of the selected type.
selectElement.addEventListener('change', function() {
const selectedType = this.value;
if (selectedType) {
resetForTypeFetch(); // Resets UI and fetch state.
fetchPokemonByType(selectedType); // Fetches Pokémon of the selected type.
updatePageBackground(selectedType); // Updates the page background to match the selected type.
} else {
// Optionally resets the background if no type is selected.
document.body.style.backgroundImage = ''; // Resets to default background.
}
});
}
setupTypeSelector(); // Initializes the type selector setup.
// Resets UI and fetch state when changing filters or clearing them.
function resetForTypeFetch() {
pokemonTypeOffset = 0; // Resets the offset for fetching Pokémon by type.
pokemonTypeFetched = 0; // Resets the count of fetched Pokémon by type.
document.querySelector('#my-container').innerHTML = ''; // Clears the container of Pokémon displays.
}
// Clears filters, resets UI, and reloads the initial list of Pokémon when "Clear Filters" is clicked.
document.getElementById('clear-filters').addEventListener('click', function() {
const selectElement = document.getElementById('typeSelect');
selectElement.value = ''; // Resets the type selection dropdown.
resetForTypeFetch(); // Resets UI and fetch state.
lastPage = 0; // Resets pagination offset.
document.querySelector('#my-container').innerHTML = ''; // Clears the Pokémon display container.
loadList(); // Reloads the initial list of Pokémon without any filters.
});
// Updates the page background based on the selected Pokémon type.
function updatePageBackground(pokemonType) {
const imagePath = `/imgs/${pokemonType}.jpeg`; // Constructs the path to the type-specific background image.
document.body.style.backgroundImage = `url('${imagePath}')`; // Sets the background image.
document.body.style.backgroundSize = 'cover'; // Ensures the background image covers the entire page.
document.body.style.backgroundPosition = 'center'; // Centers the background image.
}
</script>
</body>
</html>