diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..0bd724a Binary files /dev/null and b/favicon.ico differ diff --git a/favicon.png b/favicon.png new file mode 100644 index 0000000..d7ad4fd Binary files /dev/null and b/favicon.png differ diff --git a/index.html b/index.html index 958a010..b62f067 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ Pokémon Finder + @@ -55,6 +56,16 @@
+ + +
+ +
diff --git a/index.js b/index.js index 4719e31..617b172 100644 --- a/index.js +++ b/index.js @@ -1,39 +1,130 @@ -import "/style.css"; // Import the CSS file -import Pokedex from "pokedex-promise-v2"; // Use bare specifier +import "/style.css"; // Ensure CSS is imported +import Pokedex from "pokedex-promise-v2"; const options = { protocol: "https", - hostName: "pokeapi.co", // Use the actual PokeAPI host + hostName: "pokeapi.co", versionPath: "/api/v2/", - cacheLimit: 100 * 1000, // 100s - timeout: 10 * 1000, // 10s increased timeout + cacheLimit: 100 * 1000, + timeout: 10 * 1000, }; const pokemon = new Pokedex(options); +// --- DOM Elements --- const searchButton = document.getElementById("searchButton"); const pokemonNameInput = document.getElementById("pokemonName"); 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 = + '
  • No history yet.
  • '; + } 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 = ` +

    ${ + pokemonData.name.charAt(0).toUpperCase() + + pokemonData.name.slice(1) + }

    + ${
+        pokemonData.name
+      } +

    Type(s): ${pokemonData.types + .map((t) => t.type.name) + .join(", ")}

    +

    Height: ${pokemonData.height / 10} m

    +

    Weight: ${pokemonData.weight / 10} kg

    + `; + + // 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 pokemonName = data.name; // Keep name for comparison pokemonInfoDiv.innerHTML = `

    ${ - data.name.charAt(0).toUpperCase() + data.name.slice(1) + pokemonName.charAt(0).toUpperCase() + pokemonName.slice(1) }

    - ${
-    data.name
-  } + ${pokemonName}

    Type(s): ${data.types .map((typeInfo) => typeInfo.type.name) .join(", ")}

    Height: ${data.height / 10} m

    Weight: ${data.weight / 10} kg

    `; + + // --- 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) => { - pokemonInfoDiv.innerHTML = `

    Error: ${error.message}

    `; - console.error(error); + if (error && error.response && error.response.status === 404) { + pokemonInfoDiv.innerHTML = `

    Sorry, Pokémon not found. Please check the spelling and try again.

    `; + } else { + pokemonInfoDiv.innerHTML = `

    An error occurred while fetching Pokémon data. Please try again later.

    `; + console.error("Error fetching Pokémon:", error); + } }; const searchPokemon = () => { @@ -42,10 +133,11 @@ const searchPokemon = () => { pokemonInfoDiv.innerHTML = `

    Please enter a Pokémon name.

    `; return; } - pokemonInfoDiv.innerHTML = `

    Loading...

    `; // Provide loading feedback + pokemonInfoDiv.innerHTML = `

    Loading...

    `; pokemon.getPokemonByName(name).then(displayPokemonInfo).catch(displayError); }; +// --- Event Listeners & Voice Search --- searchButton.addEventListener("click", searchPokemon); pokemonNameInput.addEventListener("keypress", (event) => { @@ -54,55 +146,56 @@ pokemonNameInput.addEventListener("keypress", (event) => { } }); -// --- Voice Search --- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; let recognition; if (SpeechRecognition) { recognition = new SpeechRecognition(); - recognition.continuous = false; // Only listen for a single utterance + recognition.continuous = false; recognition.lang = "en-US"; - recognition.interimResults = false; // We only want final results + recognition.interimResults = false; recognition.maxAlternatives = 1; voiceSearchButton.addEventListener("click", () => { try { pokemonNameInput.placeholder = "Listening..."; recognition.start(); - voiceSearchButton.classList.add("text-red-500"); // Indicate listening + voiceSearchButton.classList.add("text-red-500"); } catch (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) => { const speechResult = event.results[0][0].transcript.toLowerCase(); pokemonNameInput.value = speechResult; - // Optionally trigger search immediately after recognition searchPokemon(); }; recognition.onspeechend = () => { recognition.stop(); 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) => { pokemonInfoDiv.innerHTML = `

    Voice recognition error: ${event.error}

    `; console.error("Speech recognition error:", event); 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) => { pokemonInfoDiv.innerHTML = `

    Didn't recognize that Pokémon name.

    `; 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 { 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 diff --git a/style.css b/style.css index 153929b..a43d322 100644 --- a/style.css +++ b/style.css @@ -2,8 +2,40 @@ @tailwind components; @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 { - /* Example: Ensure gradient covers full height */ min-height: 100vh; }