diff --git a/src/App.jsx b/src/App.jsx
index 9bc0e21..87ecba6 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -42,6 +42,7 @@ import Pets from './pages/Pets.jsx';
import Covid from './pages/Covid.jsx';
import Navbar from './components/Navbar.jsx';
import ContributorsWall from './pages/Contributors.jsx'
+import Pokedex from './pages/Pokedex.jsx';
// TODO: Extract theme state into context (see todo 5).
import { useState, useEffect } from 'react';
@@ -79,7 +80,8 @@ export default function App() {
} />
} />
} />
- } />
+ } />
+ } />
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
index a318450..e1be9cc 100644
--- a/src/pages/Home.jsx
+++ b/src/pages/Home.jsx
@@ -33,7 +33,9 @@ const dashboards = [
{ path: '/jokes-quotes', title: 'Jokes & Quotes', desc: 'Random jokes & inspiration' },
{ path: '/pets', title: 'Pets Images', desc: 'Random dog & cat images' },
{ path: '/covid', title: 'COVID-19 Stats', desc: 'Global & country data' },
+ { path: '/pokedex', title: 'Pokédex', desc: 'Explore Pokémon species' },
{ path: '/contributors', title: 'Contributor Wall', desc: 'Our Contributors' },
+
];
export default function Home() {
diff --git a/src/pages/Pokedex.jsx b/src/pages/Pokedex.jsx
new file mode 100644
index 0000000..b4cf85b
--- /dev/null
+++ b/src/pages/Pokedex.jsx
@@ -0,0 +1,387 @@
+// src/components/Pokedex.jsx
+import React, { useEffect, useState, useRef, useMemo } from "react";
+
+export default function Pokedex() {
+ const [pokemon, setPokemon] = useState([]);
+ const [search, setSearch] = useState("");
+ const [loadingNames, setLoadingNames] = useState(true);
+ const [loadingDetails, setLoadingDetails] = useState(true);
+ const [allNames, setAllNames] = useState([]);
+ const debounceRef = useRef(null);
+
+ // On mount: fetch all names (lightweight) and then load initial details for first page
+ useEffect(() => {
+ let mounted = true;
+
+ async function init() {
+ try {
+ // fetch all names (small payload of names/urls)
+ const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=100000");
+ const list = await res.json();
+ if (!mounted) return;
+ const names = list.results.map((r) => r.name);
+ setAllNames(names);
+ setLoadingNames(false);
+
+ // Fetch details for first 60 to populate initial UI
+ const initial = names.slice(0, 60);
+ await fetchDetailsForNames(initial);
+ } catch (e) {
+ console.error("fetch error", e);
+ setLoadingNames(false);
+ setLoadingDetails(false);
+ }
+ }
+
+ init();
+ return () => {
+ mounted = false;
+ };
+ }, []);
+
+ // Fetch details helper (accepts array of names)
+ async function fetchDetailsForNames(names) {
+ if (!names || names.length === 0) return;
+ setLoadingDetails(true);
+ try {
+ // Only fetch those we don't already have
+ const existing = new Set(pokemon.map((p) => p.name));
+ const toFetch = names.filter((n) => !existing.has(n));
+ if (toFetch.length === 0) return;
+
+ const details = await Promise.all(
+ toFetch.map(async (name) => {
+ try {
+ const r = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`);
+ if (!r.ok) throw new Error(`not found ${name}`);
+ return await r.json();
+ } catch (err) {
+ // swallow individual errors
+ return null;
+ }
+ })
+ );
+
+ const valid = details.filter(Boolean);
+ if (valid.length > 0) {
+ setPokemon((prev) => {
+ // merge without duplicates
+ const map = new Map(prev.map((p) => [p.name, p]));
+ valid.forEach((d) => map.set(d.name, d));
+ return Array.from(map.values());
+ });
+ }
+ } catch (e) {
+ console.error("detail fetch error", e);
+ } finally {
+ setLoadingDetails(false);
+ }
+ }
+
+ // Build a map for quick lookup
+ const pokemonByName = useMemo(() => new Map(pokemon.map((p) => [p.name, p])), [pokemon]);
+
+ // Determine the list of names to display based on search; cap results to 60
+ const matchedNames = useMemo(() => {
+ const q = search.trim().toLowerCase();
+ if (!q) return allNames.slice(0, 60);
+ return allNames.filter((n) => n.includes(q)).slice(0, 60);
+ }, [search, allNames]);
+
+ // Array of pokemon details to render in the matched order
+ const filtered = matchedNames.map((n) => pokemonByName.get(n)).filter(Boolean);
+
+ // Debounced effect: when `search` changes, fetch details for the top matches
+ useEffect(() => {
+ // clear previous debounce
+ if (debounceRef.current) {
+ clearTimeout(debounceRef.current);
+ }
+
+ // If there's no names loaded yet, nothing to do
+ if (!allNames || allNames.length === 0) return;
+
+ debounceRef.current = setTimeout(() => {
+ // find matches (top 40)
+ const q = search.trim().toLowerCase();
+ const namesToLoad = q ? allNames.filter((n) => n.includes(q)).slice(0, 40) : allNames.slice(0, 40);
+ fetchDetailsForNames(namesToLoad);
+ }, 260);
+
+ return () => {
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ };
+ }, [search, allNames]);
+
+ // Inline style objects for a few dynamic bits:
+ const containerStyle = {
+ minHeight: "100vh",
+ padding: "44px 20px",
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ color: "#111",
+ fontFamily: "'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial",
+ };
+
+ const titleStyle = {
+ fontSize: "3rem",
+ fontWeight: 900,
+ color: "#fff",
+ letterSpacing: "3px",
+ textShadow: "0 6px 14px rgba(0,0,0,0.35)",
+ margin: "0 0 18px 0",
+ };
+
+ const inputStyle = {
+ width: "280px",
+ padding: "10px 14px",
+ borderRadius: "26px",
+ border: "none",
+ outline: "none",
+ textAlign: "center",
+ fontWeight: 700,
+ fontSize: "1rem",
+ marginBottom: "28px",
+ boxShadow: "0 6px 18px rgba(0,0,0,0.18)",
+ };
+
+ const gridStyle = {
+ display: "grid",
+ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
+ gap: "24px",
+ width: "100%",
+ maxWidth: "1120px",
+ };
+
+ const cardBaseStyle = {
+ position: "relative",
+ borderRadius: "18px",
+ padding: "18px",
+ textAlign: "center",
+ overflow: "hidden",
+ cursor: "pointer",
+ transition: "transform .28s cubic-bezier(.2,.9,.2,1), box-shadow .28s ease, filter .28s ease",
+ boxShadow: "0 8px 22px rgba(0,0,0,0.18)",
+ willChange: "transform",
+ userSelect: "none",
+ };
+
+ return (
+
+ {/* Embedded CSS (keyframes, classes, responsive tweaks). This keeps everything in one file. */}
+
+
+
+
+
PokéDex
+
+
setSearch(e.target.value)}
+ />
+
+ {(loadingNames || (loadingDetails && pokemon.length === 0)) ? (
+
+ ) : (
+
+
+
+ {filtered.map((p) => {
+ // fallback color by primary type:
+ const primaryType = p.types[0]?.type?.name || "normal";
+ const typeColors = {
+ fire: ["#ff9a65", "#ff6b6b"],
+ water: ["#8ec5ff", "#5aa3ff"],
+ grass: ["#a8ff8c", "#3bb78f"],
+ electric: ["#ffd56b", "#ffcc33"],
+ psychic: ["#ff9ad5", "#ff6fb1"],
+ rock: ["#d6d6a3", "#b29b6b"],
+ ground: ["#efd6b8", "#d29b5b"],
+ bug: ["#d8f3a6", "#8ecb3f"],
+ poison: ["#d6a8ff", "#9b67ff"],
+ fairy: ["#ffdff6", "#ff9adf"],
+ fighting: ["#ffb59a", "#ff7043"],
+ flying: ["#cfe9ff", "#9ad0ff"],
+ ghost: ["#c5b7ff", "#8f77ff"],
+ dragon: ["#b8e1ff", "#6fb3ff"],
+ ice: ["#dff8ff", "#8fe9ff"],
+ dark: ["#c6b8b0", "#8b7f70"],
+ steel: ["#d7e0e6", "#9fb0bd"],
+ normal: ["#f5f5f5", "#dcdcdc"]
+ };
+ const colors = typeColors[primaryType] || typeColors.normal;
+ const cardStyle = {
+ ...cardBaseStyle,
+ background: `linear-gradient(135deg, ${colors[0]}, ${colors[1]})`,
+ color: "#111",
+ };
+
+ return (
+
{}}
+ >
+
+

{
+ e.currentTarget.style.opacity = 0.6;
+ }}
+ />
+
+
+ {p.name}
+
+
+
+ {p.types.map((t) => (
+
+ {t.type.name}
+
+ ))}
+
+
+

+
+
+ );
+ })}
+
+
+
+ )}
+
+ );
+}