curriculum/challenges/english/blocks/build-an-rpg-creature-search-app-project/6555c1d3e11a1574434cf8b5.md
In this project, you'll build an app that will search for creatures from an RPG game by name or ID and display the results to the user. To retrieve the creature data and images, you'll use freeCodeCamp's <a href="https://rpg-creature-api.freecodecamp.rocks/" target="_blank" rel="noopener noreferrer nofollow">RPG Creature API</a>.
Note: The first 13 steps must be completed inside the index.html file.
Objective: Build an app that is functionally similar to <a href="https://rpg-creature-search-app.freecodecamp.rocks" target="_blank" rel="noopener noreferrer nofollow">https://rpg-creature-search-app.freecodecamp.rocks</a>.
User Stories:
input element with an id of "search-input", and is required.button element with an id of "search-button".id of "creature-name".id of "creature-id".id of "weight".id of "height".id of "types".id of "hp".id of "attack".id of "defense".id of "special-attack".id of "special-defense".id of "speed".#search-input element contains the value Red and the #search-button element is clicked, an alert should appear with the text "Creature not found".#search-input element contains the value Pyrolynx and the #search-button element is clicked, the values in the #creature-name, #creature-id, #weight, #height, #hp, #attack, #defense, #special-attack, #special-defense, and #speed elements should be PYROLYNX, #1 or 1, Weight: 42 or 42, Height: 32 or 32, 65, 80, 50, 90, 55, and 100, respectively.#search-input element contains the value Pyrolynx and the #search-button element is clicked, a single element should be added within the #types element that contains the text FIRE. The #types element content should be cleared between searches.#search-input element contains the value 2 and the #search-button element is clicked, the values in the #creature-name, #creature-id, #weight, #height, #hp, #attack, #defense, #special-attack, #special-defense, and #speed elements should be AQUOROC, #2 or 2, Weight: 220 or 220, Height: 53 or 53, 85, 90, 120, 60, 70, and 40, respectively.#search-input element contains the value 2 and the #search-button element is clicked, two elements should be added within the #types element that contain text values WATER and ROCK, respectively. The #types element content should be cleared between searches.#search-input element contains an invalid creature name, and the #search-button element is clicked, an alert should appear with the text "Creature not found".#search-input element contains a valid creature ID and the #search-button element is clicked, the UI should be filled with the correct data.Fulfill the user stories and pass all the tests below to complete this project. Give it your own personal style. Happy Coding!
Note: When running the tests there will be a slight delay. Please wait a few seconds to allow the tests to finish. Do not refresh the page before they are done.
You should have an input element with an id of "search-input". The input should be marked as required.
const el = document.getElementById('search-input');
assert.strictEqual(el?.nodeName?.toLowerCase(), 'input');
assert.isTrue(el?.required);
You should have a button element with an id of "search-button".
const el = document.getElementById('search-button');
assert.strictEqual(el?.nodeName?.toLowerCase(), 'button');
You should have an element with an id of "creature-name".
const el = document.getElementById('creature-name');
assert.exists(el);
You should have an element with an id of "creature-id".
const el = document.getElementById('creature-id');
assert.exists(el);
You should have an element with an id of "weight".
const el = document.getElementById('weight');
assert.exists(el);
You should have an element with an id of "height".
const el = document.getElementById('height');
assert.exists(el);
You should have an element with an id of "types".
const el = document.getElementById('types');
assert.exists(el);
You should have an element with an id of "hp".
const el = document.getElementById('hp');
assert.exists(el);
You should have an element with an id of "attack".
const el = document.getElementById('attack');
assert.exists(el);
You should have an element with an id of "defense".
const el = document.getElementById('defense');
assert.exists(el);
You should have an element with an id of "special-attack".
const el = document.getElementById('special-attack');
assert.exists(el);
You should have an element with an id of "special-defense".
const el = document.getElementById('special-defense');
assert.exists(el);
You should have an element with an id of "speed".
const el = document.getElementById('speed');
assert.exists(el);
When the #search-input element contains the value Red and the #search-button element is clicked, an alert should appear with the text "Creature not found".
try {
const searchInput = document.getElementById('search-input');
const searchButton = document.getElementById('search-button');
let alertMessage;
window.alert = (message) => alertMessage = message; // Override alert and store message
searchInput.value = 'Red';
searchInput.dispatchEvent(new Event('change'));
searchButton.click();
const res = await fetch('https://rpg-creature-api.freecodecamp.rocks/api/creature/red'); // Fetch from proxy to simulate network delay
if (!res.ok) {
await new Promise(resolve => setTimeout(resolve, 1000)); // Additional delay to allow the alert to trigger
assert.equal(alertMessage?.trim().replace(/[.,?!]+$/g, '').toLowerCase(), 'creature not found');
} else {
assert.fail();
}
} catch (err) {
throw new Error(err);
}
When the #search-input element contains the value Pyrolynx and the #search-button element is clicked, the values in the #creature-name, #creature-id, #weight, #height, #hp, #attack, #defense, #special-attack, #special-defense, and #speed elements should be PYROLYNX, #1 or 1, Weight: 42 or 42, Height: 32 or 32, 65, 80, 50, 90, 55, and 100, respectively.
try {
const searchInput = document.getElementById('search-input');
const searchButton = document.getElementById('search-button');
searchInput.value = 'Pyrolynx';
searchInput.dispatchEvent(new Event('change'));
searchButton.click();
const res = await fetch('https://rpg-creature-api.freecodecamp.rocks/api/creature/Pyrolynx'); // Fetch from proxy to simulate network delay
if (res.ok) {
await new Promise(resolve => setTimeout(resolve, 1000)); // Additional delay to allow UI to update
const creatureName = document.getElementById('creature-name');
const creatureID = document.getElementById('creature-id');
const weight = document.getElementById('weight');
const height = document.getElementById('height');
const hp = document.getElementById('hp');
const attack = document.getElementById('attack');
const defense = document.getElementById('defense');
const specialAttack = document.getElementById('special-attack');
const specialDefense = document.getElementById('special-defense');
const speed = document.getElementById('speed');
assert.strictEqual(creatureName.innerText.trim().toLowerCase(), 'pyrolynx');
assert.include(['#1', '1'], creatureID.innerText.trim());
assert.include(['weight: 42', '42'], weight.innerText.trim().toLowerCase());
assert.include(['height: 32', '32'], height.innerText.trim().toLowerCase());
assert.strictEqual(hp.innerText.trim(), '65');
assert.strictEqual(attack.innerText.trim(), '80');
assert.strictEqual(defense.innerText.trim(), '50');
assert.strictEqual(specialAttack.innerText.trim(), '90');
assert.strictEqual(specialDefense.innerText.trim(), '55');
assert.strictEqual(speed.innerText.trim(), '100');
} else {
assert.fail();
}
} catch (err) {
throw new Error(err);
}
When the #search-input element contains the value Pyrolynx and the #search-button element is clicked, a single element should be added within the #types element that contains the text FIRE. The #types element content should be cleared between searches.
try {
const searchInput = document.getElementById('search-input');
const searchButton = document.getElementById('search-button');
searchInput.value = 'Pyrolynx';
searchInput.dispatchEvent(new Event('change'));
searchButton.click();
const res = await fetch('https://rpg-creature-api.freecodecamp.rocks/api/creature/Pyrolynx'); // Fetch from proxy to simulate network delay
if (res.ok) {
await new Promise(resolve => setTimeout(resolve, 1000)); // Additional delay to allow UI to update
const typesEl = document.getElementById('types');
assert.lengthOf(typesEl.children, 1);
assert.strictEqual(typesEl?.children[0]?.innerText.trim().toLowerCase(), 'fire');
} else {
assert.fail();
}
} catch (err) {
throw new Error(err);
}
When the #search-input element contains the value 2 and the #search-button element is clicked, the values in the #creature-name, #creature-id, #weight, #height, #hp, #attack, #defense, #special-attack, #special-defense, and #speed elements should be AQUOROC, #2 or 2, Weight: 220 or 220, Height: 53 or 53, 85, 90, 120, 60, 70, and 40, respectively.
try {
const searchInput = document.getElementById('search-input');
const searchButton = document.getElementById('search-button');
searchInput.value = '2';
searchInput.dispatchEvent(new Event('change'));
searchButton.click();
const res = await fetch('https://rpg-creature-api.freecodecamp.rocks/api/creature/2'); // Fetch from proxy to simulate network delay
if (res.ok) {
await new Promise(resolve => setTimeout(resolve, 1000)); // Additional delay to allow UI to update
const creatureName = document.getElementById('creature-name');
const creatureID = document.getElementById('creature-id');
const weight = document.getElementById('weight');
const height = document.getElementById('height');
const hp = document.getElementById('hp');
const attack = document.getElementById('attack');
const defense = document.getElementById('defense');
const specialAttack = document.getElementById('special-attack');
const specialDefense = document.getElementById('special-defense');
const speed = document.getElementById('speed');
assert.strictEqual(creatureName.innerText.trim().toLowerCase(), 'aquoroc');
assert.include(['#2', '2'], creatureID.innerText.trim());
assert.include(['weight: 220', '220'], weight.innerText.trim().toLowerCase());
assert.include(['height: 53', '53'], height.innerText.trim().toLowerCase());
assert.strictEqual(hp.innerText.trim(), '85');
assert.strictEqual(attack.innerText.trim(), '90');
assert.strictEqual(defense.innerText.trim(), '120');
assert.strictEqual(specialAttack.innerText.trim(), '60');
assert.strictEqual(specialDefense.innerText.trim(), '70');
assert.strictEqual(speed.innerText.trim(), '40');
} else {
assert.fail();
}
} catch (err) {
throw new Error(err);
}
When the #search-input element contains the value 2 and the #search-button element is clicked, two elements should be added within the #types element that contain text values WATER and ROCK, respectively. The #types element content should be cleared between searches.
try {
const searchInput = document.getElementById('search-input');
const searchButton = document.getElementById('search-button');
searchInput.value = '2';
searchInput.dispatchEvent(new Event('change'));
searchButton.click();
const res = await fetch('https://rpg-creature-api.freecodecamp.rocks/api/creature/2'); // Fetch from proxy to simulate network delay
if (res.ok) {
await new Promise(resolve => setTimeout(resolve, 1000)); // Additional delay to allow UI to update
const targetTypes = ['water', 'rock'];
const typesEl = document.getElementById('types');
assert.lengthOf(typesEl.children, 2);
assert.sameMembers(['water', 'rock'], [...typesEl.children].map(el => el.innerText.trim().toLowerCase()));
} else {
assert.fail();
}
} catch (err) {
throw new Error(err);
}
When the #search-input element contains an invalid creature name and the #search-button element is clicked, an alert should appear with the text "Creature not found".
try {
const searchInput = document.getElementById('search-input');
const searchButton = document.getElementById('search-button');
let alertMessage;
window.alert = (message) => alertMessage = message; // Override alert and store message
const randomInvalidCreatureId = crypto.randomUUID().substring(0, 6);
searchInput.value = randomInvalidCreatureId;
searchInput.dispatchEvent(new Event('change'));
searchButton.click();
const res = await fetch('https://rpg-creature-api.freecodecamp.rocks/api/creature/' + randomInvalidCreatureId); // Fetch from proxy to simulate network delay
if (!res.ok) {
await new Promise(resolve => setTimeout(resolve, 2000)); // Additional delay to allow the alert to trigger
assert.equal(alertMessage?.trim().replace(/[.,?!]+$/g, '').toLowerCase(), 'creature not found');
} else {
assert.fail();
}
} catch (err) {
throw new Error(err);
}
When the #search-input element contains a valid creature ID and the #search-button element is clicked, the UI should be filled with the correct data.
try {
const searchInput = document.getElementById('search-input');
const searchButton = document.getElementById('search-button');
let alertMessage;
window.alert = (message) => alertMessage = message; // Override alert and store message
const randomValidCreatureId = String(Math.floor(Math.random() * 20) + 1);
searchInput.value = randomValidCreatureId;
searchInput.dispatchEvent(new Event('change'));
searchButton.click();
const res = await fetch('https://rpg-creature-api.freecodecamp.rocks/api/creature/' + randomValidCreatureId); // Fetch from proxy to simulate network delay
if (res.ok) {
await new Promise(resolve => setTimeout(resolve, 2000)); // Additional delay to allow UI to update
const data = await res.json();
const typesEl = document.getElementById('types');
const actualTypes = data.types.map(typeSlot => typeSlot.name);
assert.lengthOf(typesEl.children, actualTypes.length);
assert.sameMembers(actualTypes, [...typesEl.children].map(el => el.innerText.trim().toLowerCase()));
} else {
assert.fail();
}
} catch (err) {
throw new Error(err);
}
When the search button is clicked, the app should send a fetch request to the correct endpoint for the creature name or ID.
const spy = __helpers.spyOn(window, 'fetch');
const searchInput = document.getElementById('search-input');
const searchButton = document.getElementById('search-button');
searchInput.value = 'Pyrolynx';
searchInput.dispatchEvent(new Event('change'));
searchButton.click();
await new Promise((resolve) => setTimeout(resolve, 500));
searchInput.value = '2';
searchInput.dispatchEvent(new Event('change'));
searchButton.click();
await new Promise((resolve) => setTimeout(resolve, 500));
// Test with random valid ID
const randomValidCreatureId = String(Math.floor(Math.random() * 20) + 1);
searchInput.value = randomValidCreatureId;
searchInput.dispatchEvent(new Event('change'));
searchButton.click();
await new Promise((resolve) => setTimeout(resolve, 500));
const calls = spy.calls.map((call) => call[0]);
assert.strictEqual(calls[0].toLowerCase(), 'https://rpg-creature-api.freecodecamp.rocks/api/creature/pyrolynx');
assert.strictEqual(calls[1], 'https://rpg-creature-api.freecodecamp.rocks/api/creature/2');
assert.strictEqual(calls[2], `https://rpg-creature-api.freecodecamp.rocks/api/creature/${randomValidCreatureId}`);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="icon"
type="image/png"
href="https://cdn.freecodecamp.org/universal/favicons/favicon.ico"
/>
<title>Creature Search App</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main>
<h1>Creature Search App</h1>
<div class="container">
<form role="search" id="search-form">
<label for="search-input">Search for Creature Name or ID:</label>
<input type="text" name="creature" id="search-input" required />
<button id="search-button">Search</button>
</form>
<div class="output">
<div class="top-container">
<div class="name-and-id">
<span id="creature-name"></span>
<span id="creature-id"></span>
<div class="size">
<span id="weight"></span>
<span id="height"></span>
</div>
</div>
<div id="types"></div>
<div>
<div id="special-name"></div>
<div id="special-description"></div>
</div>
</div>
<div class="bottom-container">
<table>
<tr>
<th>Base</th>
<th>Stats</th>
</tr>
<tr>
<td>HP:</td>
<td id="hp"></td>
</tr>
<tr>
<td>Attack:</td>
<td id="attack"></td>
</tr>
<tr>
<td>Defense:</td>
<td id="defense"></td>
</tr>
<tr>
<td>Sp. Attack:</td>
<td id="special-attack"></td>
</tr>
<tr>
<td>Sp. Defense:</td>
<td id="special-defense"></td>
</tr>
<tr>
<td>Speed:</td>
<td id="speed" class="speed"></td>
</tr>
</table>
</div>
</div>
</div>
<script src="./script.js"></script>
</main>
</body>
</html>
/* CSS reset */
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
body {
line-height: 1.5;
}
img {
display: block;
}
/* Project styling */
body {
height: 100vh;
font-family: sans-serif;
background-color: #1b1b32;
color: #0a0a23;
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.freecodecamp-logo {
height: 30px;
margin: 25px 0;
}
h1 {
color: #f5f6f7;
font-size: 1.7em;
text-align: center;
}
.container {
width: 325px;
margin: 25px 0;
background-color: #f5f6f7;
border: 1px solid #f5f6f7;
border-radius: 15px;
box-shadow: 10px 10px 0px 0px rgba(223, 220, 220, 0.75);
}
.output,
#search-form {
display: flex;
justify-content: center;
}
#search-form {
flex-wrap: wrap;
margin: 10px 0;
padding: 5px;
border-radius: 8px 8px 0 0;
gap: 10px;
}
label {
align-self: center;
}
#search-input:focus-visible,
#search-button:focus-visible {
outline: 3px solid #198eee;
}
#search-input {
height: 40px;
padding-left: 10px;
width: 200px;
}
#search-button {
padding: 14px 0;
width: 80px;
border-radius: 20px;
text-align: center;
background-color: #7f21ab;
color: #f5f6f7;
border: none;
}
.output {
margin: 10px 0;
padding: 5px;
flex-direction: column;
align-items: center;
}
.top-container,
.bottom-container {
display: flex;
flex-direction: column;
width: 100%;
}
.top-container {
display: flex;
flex-direction: column;
justify-content: space-evenly;
margin-bottom: 10px;
padding: 10px;
background-color: #f0f1f7;
min-height: 200px;
}
.bottom-container {
min-height: 325px;
}
.name-and-id {
height: 28px;
font-size: 1.1em;
text-transform: capitalize;
margin-bottom: 5px;
}
#creature-name,
#special-name {
font-weight: bold;
}
.size,
#special-description {
font-size: 0.85rem;
}
#types {
min-height: 30px;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 5px;
}
.type {
width: 66px;
padding: 5px;
font-size: 0.7rem;
text-align: center;
border-radius: 5px;
background-color: red;
text-transform: uppercase;
}
table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
font-size: 1 rem;
color: #f5f6f7;
background-color: #7f21ab;
}
th:nth-child(even),
td:nth-child(even) {
border-left: 5px solid #f5f6f7;
}
tr {
border-bottom: 5px solid #f5f6f7;
}
td,
th {
text-align: center;
padding: 8px;
}
/* Special styling for Creature types */
.normal {
background-color: #b7b7aa;
}
.fire {
background-color: #ff6f52;
}
.water {
background-color: #42a1ff;
}
.electric {
background-color: #fecc33;
}
.grass {
background-color: #78cc55;
}
.ice {
background-color: #66ccfe;
}
.fighting {
background-color: #d3887e;
}
.poison {
background-color: #c68bb7;
}
.ground {
background-color: #dfba52;
}
.flying {
background-color: #8899ff;
}
.psychic {
background-color: #ff66a3;
}
.bug {
background-color: #aabb23;
}
.rock {
background-color: #baaa66;
}
.ghost {
background-color: #9995d0;
}
.dragon {
background-color: #9e93f1;
}
.dark {
background-color: #b59682;
}
.steel {
background-color: #abaabb;
}
.fairy {
background-color: #ed99ed;
}
@media screen and (min-width: 550px) {
h1 {
font-size: 2em;
}
.container {
width: 450px;
}
#search-form,
.top-container,
.bottom-container {
width: 95%;
}
.type {
width: 75px;
}
}
const creatureID = document.getElementById('creature-id');
const creatureName = document.getElementById('creature-name');
const specialName = document.getElementById('special-name');
const specialDescription = document.getElementById('special-description');
const types = document.getElementById('types');
const height = document.getElementById('height');
const weight = document.getElementById('weight');
const hp = document.getElementById('hp');
const attack = document.getElementById('attack');
const defense = document.getElementById('defense');
const specialAttack = document.getElementById('special-attack');
const specialDefense = document.getElementById('special-defense');
const speed = document.getElementById('speed');
const searchForm = document.getElementById('search-form');
const searchInput = document.getElementById('search-input');
const getCreature = async () => {
try {
const creatureNameOrId = searchInput.value.toLowerCase();
const response = await fetch(
`https://rpg-creature-api.freecodecamp.rocks/api/creature/${creatureNameOrId}`
);
const data = await response.json();
// Set Creature info
creatureName.textContent = `${data.name.toUpperCase()}`;
creatureID.textContent = `#${data.id}`;
weight.textContent = `Weight: ${data.weight}`;
height.textContent = `Height: ${data.height}`;
specialName.textContent = data.special.name;
specialDescription.textContent = data.special.description;
// Set stats
hp.textContent = data.stats[0].base_stat;
attack.textContent = data.stats[1].base_stat;
defense.textContent = data.stats[2].base_stat;
specialAttack.textContent = data.stats[3].base_stat;
specialDefense.textContent = data.stats[4].base_stat;
speed.textContent = data.stats[5].base_stat;
// Set types
types.innerHTML = data.types
.map(obj => `<span class="type ${obj.name}">${obj.name}</span>`)
.join('');
} catch (err) {
resetDisplay();
alert('Creature not found');
console.log(`Creature not found: ${err}`);
}
};
const resetDisplay = () => {
// reset stats
creatureName.textContent = '';
creatureID.textContent = '';
height.textContent = '';
weight.textContent = '';
types.innerHTML = '';
specialName.innerHTML = '';
specialDescription.innerHTML = '';
hp.textContent = '';
attack.textContent = '';
defense.textContent = '';
specialAttack.textContent = '';
specialDefense.textContent = '';
speed.textContent = '';
};
searchForm.addEventListener('submit', e => {
e.preventDefault();
getCreature();
});