mirror of
https://github.com/acedanger/pokemon.git
synced 2025-12-05 14:40:14 -08:00
feat: add favicon files and implement search history feature with animations
This commit is contained in:
BIN
favicon.ico
Normal file
BIN
favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
BIN
favicon.png
Normal file
BIN
favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
13
index.html
13
index.html
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Pokémon Finder</title>
|
<title>Pokémon Finder</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 1 1 6 0v8.25a3 3 0 0 1-3 3Z"
|
d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 1 1-6 0v8.25a3 3 0 0 1-3 3Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -55,6 +56,16 @@
|
|||||||
<div id="pokemonInfo" class="mt-6 text-center text-gray-700">
|
<div id="pokemonInfo" class="mt-6 text-center text-gray-700">
|
||||||
<!-- Pokémon info will be displayed here -->
|
<!-- Pokémon info will be displayed here -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- History Section - Removed h3 and border -->
|
||||||
|
<div id="searchHistory" class="mt-8 pt-4">
|
||||||
|
<ul id="historyList" class="list-none space-y-4">
|
||||||
|
<!-- History items (full cards) will be added here -->
|
||||||
|
<li class="italic text-sm text-center text-gray-600">
|
||||||
|
No history yet.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/index.js"></script>
|
<script type="module" src="/index.js"></script>
|
||||||
|
|||||||
139
index.js
139
index.js
@@ -1,39 +1,130 @@
|
|||||||
import "/style.css"; // Import the CSS file
|
import "/style.css"; // Ensure CSS is imported
|
||||||
import Pokedex from "pokedex-promise-v2"; // Use bare specifier
|
import Pokedex from "pokedex-promise-v2";
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostName: "pokeapi.co", // Use the actual PokeAPI host
|
hostName: "pokeapi.co",
|
||||||
versionPath: "/api/v2/",
|
versionPath: "/api/v2/",
|
||||||
cacheLimit: 100 * 1000, // 100s
|
cacheLimit: 100 * 1000,
|
||||||
timeout: 10 * 1000, // 10s increased timeout
|
timeout: 10 * 1000,
|
||||||
};
|
};
|
||||||
const pokemon = new Pokedex(options);
|
const pokemon = new Pokedex(options);
|
||||||
|
|
||||||
|
// --- DOM Elements ---
|
||||||
const searchButton = document.getElementById("searchButton");
|
const searchButton = document.getElementById("searchButton");
|
||||||
const pokemonNameInput = document.getElementById("pokemonName");
|
const pokemonNameInput = document.getElementById("pokemonName");
|
||||||
const pokemonInfoDiv = document.getElementById("pokemonInfo");
|
const pokemonInfoDiv = document.getElementById("pokemonInfo");
|
||||||
const voiceSearchButton = document.getElementById("voiceSearchButton"); // Get voice button
|
const voiceSearchButton = document.getElementById("voiceSearchButton");
|
||||||
|
const historyList = document.getElementById("historyList");
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
let searchHistory = []; // Now stores full data objects
|
||||||
|
const MAX_HISTORY_LENGTH = 3;
|
||||||
|
let lastDisplayedPokemon = null; // Track the last displayed Pokémon
|
||||||
|
|
||||||
|
// --- Functions ---
|
||||||
|
|
||||||
|
const updateHistoryDisplay = (justAddedName = null) => {
|
||||||
|
historyList.innerHTML = ""; // Clear current list visually
|
||||||
|
if (searchHistory.length === 0) {
|
||||||
|
historyList.innerHTML =
|
||||||
|
'<li class="italic text-sm text-center text-gray-600">No history yet.</li>';
|
||||||
|
} else {
|
||||||
|
searchHistory.forEach((pokemonData, index) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.classList.add("cursor-pointer"); // Make whole card clickable
|
||||||
|
|
||||||
|
// Determine scale based on index
|
||||||
|
let scale = 1.0; // Default scale for the newest item
|
||||||
|
if (index === 1) scale = 0.8;
|
||||||
|
else if (index === 2) scale = 0.6;
|
||||||
|
|
||||||
|
// Apply scale via inline style
|
||||||
|
li.style.transform = `scale(${scale})`;
|
||||||
|
|
||||||
|
// Build the inner HTML for the history card
|
||||||
|
li.innerHTML = `
|
||||||
|
<h4 class="text-center">${
|
||||||
|
pokemonData.name.charAt(0).toUpperCase() +
|
||||||
|
pokemonData.name.slice(1)
|
||||||
|
}</h4>
|
||||||
|
<img src="${pokemonData.sprites.front_default}" alt="${
|
||||||
|
pokemonData.name
|
||||||
|
}">
|
||||||
|
<p><strong>Type(s):</strong> ${pokemonData.types
|
||||||
|
.map((t) => t.type.name)
|
||||||
|
.join(", ")}</p>
|
||||||
|
<p><strong>Height:</strong> ${pokemonData.height / 10} m</p>
|
||||||
|
<p><strong>Weight:</strong> ${pokemonData.weight / 10} kg</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add click listener to the card
|
||||||
|
li.addEventListener("click", () => {
|
||||||
|
pokemonNameInput.value = pokemonData.name; // Set input value
|
||||||
|
searchPokemon(); // Trigger search
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animation for the newest item
|
||||||
|
if (index === 0 && pokemonData.name === justAddedName) {
|
||||||
|
li.classList.add("history-item-enter");
|
||||||
|
// Apply initial scale for animation start
|
||||||
|
li.style.transform = `translateX(-50px) scale(${scale * 0.9})`; // Start smaller
|
||||||
|
setTimeout(() => {
|
||||||
|
li.classList.remove("history-item-enter");
|
||||||
|
// Transition back to the target scale
|
||||||
|
li.style.transform = `scale(${scale})`;
|
||||||
|
}, 10); // Small delay
|
||||||
|
}
|
||||||
|
|
||||||
|
historyList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const displayPokemonInfo = (data) => {
|
const displayPokemonInfo = (data) => {
|
||||||
|
const pokemonName = data.name; // Keep name for comparison
|
||||||
pokemonInfoDiv.innerHTML = `
|
pokemonInfoDiv.innerHTML = `
|
||||||
<h2 class="text-2xl font-semibold mb-2">${
|
<h2 class="text-2xl font-semibold mb-2">${
|
||||||
data.name.charAt(0).toUpperCase() + data.name.slice(1)
|
pokemonName.charAt(0).toUpperCase() + pokemonName.slice(1)
|
||||||
}</h2>
|
}</h2>
|
||||||
<img src="${data.sprites.front_default}" alt="${
|
<img src="${
|
||||||
data.name
|
data.sprites.front_default
|
||||||
}" class="mx-auto mb-4 w-32 h-32">
|
}" alt="${pokemonName}" class="mx-auto mb-4 w-32 h-32">
|
||||||
<p><strong>Type(s):</strong> ${data.types
|
<p><strong>Type(s):</strong> ${data.types
|
||||||
.map((typeInfo) => typeInfo.type.name)
|
.map((typeInfo) => typeInfo.type.name)
|
||||||
.join(", ")}</p>
|
.join(", ")}</p>
|
||||||
<p><strong>Height:</strong> ${data.height / 10} m</p>
|
<p><strong>Height:</strong> ${data.height / 10} m</p>
|
||||||
<p><strong>Weight:</strong> ${data.weight / 10} kg</p>
|
<p><strong>Weight:</strong> ${data.weight / 10} kg</p>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// --- Update History ---
|
||||||
|
// Only add the previous Pokémon to history (not the current one)
|
||||||
|
if (lastDisplayedPokemon && lastDisplayedPokemon.name !== data.name) {
|
||||||
|
// Remove if already exists
|
||||||
|
searchHistory = searchHistory.filter(
|
||||||
|
(item) => item.name !== lastDisplayedPokemon.name
|
||||||
|
);
|
||||||
|
// Add to beginning
|
||||||
|
searchHistory.unshift(lastDisplayedPokemon);
|
||||||
|
// Limit history length
|
||||||
|
if (searchHistory.length > MAX_HISTORY_LENGTH) {
|
||||||
|
searchHistory = searchHistory.slice(0, MAX_HISTORY_LENGTH);
|
||||||
|
}
|
||||||
|
updateHistoryDisplay(lastDisplayedPokemon.name);
|
||||||
|
} else {
|
||||||
|
updateHistoryDisplay();
|
||||||
|
}
|
||||||
|
// Update lastDisplayedPokemon to current
|
||||||
|
lastDisplayedPokemon = data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayError = (error) => {
|
const displayError = (error) => {
|
||||||
pokemonInfoDiv.innerHTML = `<p class="text-red-500">Error: ${error.message}</p>`;
|
if (error && error.response && error.response.status === 404) {
|
||||||
console.error(error);
|
pokemonInfoDiv.innerHTML = `<p class="text-orange-600">Sorry, Pokémon not found. Please check the spelling and try again.</p>`;
|
||||||
|
} else {
|
||||||
|
pokemonInfoDiv.innerHTML = `<p class="text-red-500">An error occurred while fetching Pokémon data. Please try again later.</p>`;
|
||||||
|
console.error("Error fetching Pokémon:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchPokemon = () => {
|
const searchPokemon = () => {
|
||||||
@@ -42,10 +133,11 @@ const searchPokemon = () => {
|
|||||||
pokemonInfoDiv.innerHTML = `<p class="text-yellow-600">Please enter a Pokémon name.</p>`;
|
pokemonInfoDiv.innerHTML = `<p class="text-yellow-600">Please enter a Pokémon name.</p>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pokemonInfoDiv.innerHTML = `<p>Loading...</p>`; // Provide loading feedback
|
pokemonInfoDiv.innerHTML = `<p>Loading...</p>`;
|
||||||
pokemon.getPokemonByName(name).then(displayPokemonInfo).catch(displayError);
|
pokemon.getPokemonByName(name).then(displayPokemonInfo).catch(displayError);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Event Listeners & Voice Search ---
|
||||||
searchButton.addEventListener("click", searchPokemon);
|
searchButton.addEventListener("click", searchPokemon);
|
||||||
|
|
||||||
pokemonNameInput.addEventListener("keypress", (event) => {
|
pokemonNameInput.addEventListener("keypress", (event) => {
|
||||||
@@ -54,55 +146,56 @@ pokemonNameInput.addEventListener("keypress", (event) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Voice Search ---
|
|
||||||
const SpeechRecognition =
|
const SpeechRecognition =
|
||||||
window.SpeechRecognition || window.webkitSpeechRecognition;
|
window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
let recognition;
|
let recognition;
|
||||||
|
|
||||||
if (SpeechRecognition) {
|
if (SpeechRecognition) {
|
||||||
recognition = new SpeechRecognition();
|
recognition = new SpeechRecognition();
|
||||||
recognition.continuous = false; // Only listen for a single utterance
|
recognition.continuous = false;
|
||||||
recognition.lang = "en-US";
|
recognition.lang = "en-US";
|
||||||
recognition.interimResults = false; // We only want final results
|
recognition.interimResults = false;
|
||||||
recognition.maxAlternatives = 1;
|
recognition.maxAlternatives = 1;
|
||||||
|
|
||||||
voiceSearchButton.addEventListener("click", () => {
|
voiceSearchButton.addEventListener("click", () => {
|
||||||
try {
|
try {
|
||||||
pokemonNameInput.placeholder = "Listening...";
|
pokemonNameInput.placeholder = "Listening...";
|
||||||
recognition.start();
|
recognition.start();
|
||||||
voiceSearchButton.classList.add("text-red-500"); // Indicate listening
|
voiceSearchButton.classList.add("text-red-500");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Speech recognition already started.", e);
|
console.error("Speech recognition already started.", e);
|
||||||
pokemonNameInput.placeholder = "Enter Pokémon name or use mic"; // Reset placeholder if error
|
pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
recognition.onresult = (event) => {
|
recognition.onresult = (event) => {
|
||||||
const speechResult = event.results[0][0].transcript.toLowerCase();
|
const speechResult = event.results[0][0].transcript.toLowerCase();
|
||||||
pokemonNameInput.value = speechResult;
|
pokemonNameInput.value = speechResult;
|
||||||
// Optionally trigger search immediately after recognition
|
|
||||||
searchPokemon();
|
searchPokemon();
|
||||||
};
|
};
|
||||||
|
|
||||||
recognition.onspeechend = () => {
|
recognition.onspeechend = () => {
|
||||||
recognition.stop();
|
recognition.stop();
|
||||||
pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
|
pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
|
||||||
voiceSearchButton.classList.remove("text-red-500"); // Remove listening indicator
|
voiceSearchButton.classList.remove("text-red-500");
|
||||||
};
|
};
|
||||||
|
|
||||||
recognition.onerror = (event) => {
|
recognition.onerror = (event) => {
|
||||||
pokemonInfoDiv.innerHTML = `<p class="text-red-500">Voice recognition error: ${event.error}</p>`;
|
pokemonInfoDiv.innerHTML = `<p class="text-red-500">Voice recognition error: ${event.error}</p>`;
|
||||||
console.error("Speech recognition error:", event);
|
console.error("Speech recognition error:", event);
|
||||||
pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
|
pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
|
||||||
voiceSearchButton.classList.remove("text-red-500"); // Remove listening indicator
|
voiceSearchButton.classList.remove("text-red-500");
|
||||||
};
|
};
|
||||||
|
|
||||||
recognition.onnomatch = (event) => {
|
recognition.onnomatch = (event) => {
|
||||||
pokemonInfoDiv.innerHTML = `<p class="text-yellow-600">Didn't recognize that Pokémon name.</p>`;
|
pokemonInfoDiv.innerHTML = `<p class="text-yellow-600">Didn't recognize that Pokémon name.</p>`;
|
||||||
pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
|
pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
|
||||||
voiceSearchButton.classList.remove("text-red-500"); // Remove listening indicator
|
voiceSearchButton.classList.remove("text-red-500");
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.warn("Web Speech API not supported in this browser.");
|
console.warn("Web Speech API not supported in this browser.");
|
||||||
voiceSearchButton.style.display = "none"; // Hide button if not supported
|
voiceSearchButton.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Initial Setup ---
|
||||||
|
updateHistoryDisplay(); // Initialize history display on page load
|
||||||
|
|||||||
36
style.css
36
style.css
@@ -2,8 +2,40 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
/* Add any custom base styles here if needed */
|
/* Add custom styles below */
|
||||||
|
#historyList li {
|
||||||
|
/* Slower transition */
|
||||||
|
transition: opacity 0.8s ease-out, transform 0.8s ease-out;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0) scale(1); /* Add scale to transition */
|
||||||
|
transform-origin: center; /* Scale from the center */
|
||||||
|
/* Basic card styling for history items */
|
||||||
|
@apply bg-gray-100 bg-opacity-80 rounded-lg p-3 shadow-md border border-gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
#historyList li.history-item-enter {
|
||||||
|
/* Initial state for animation */
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-50px) scale(0.9); /* Start slightly left and smaller */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling for elements within history cards */
|
||||||
|
#historyList li h4 {
|
||||||
|
@apply text-lg font-semibold mb-1 text-gray-800;
|
||||||
|
}
|
||||||
|
#historyList li img {
|
||||||
|
@apply mx-auto mb-2 w-16 h-16; /* Smaller image */
|
||||||
|
}
|
||||||
|
#historyList li p {
|
||||||
|
@apply text-xs text-gray-600 leading-tight; /* Smaller text */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder styling */
|
||||||
|
#historyList li.italic {
|
||||||
|
@apply bg-transparent shadow-none border-none p-0; /* Reset card styles for placeholder */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure gradient covers full height if not already handled by Tailwind */
|
||||||
body {
|
body {
|
||||||
/* Example: Ensure gradient covers full height */
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user