Build a Futuristic Pokedex Web UI

Wrap your BM25 search engine in a sci-fi web app with FastAPI.

Introduction

30 Second Summary

You built a search engine that finds Pokemon by name, type, or ability. It works perfectly in the terminal, but nobody else can see it or use it. A search engine locked inside a command line feels like an unfinished prototype.

In this project, you will wrap your existing search engine in a futuristic web interface. You will build a FastAPI backend that exposes your engine over HTTP, then create a sci-fi themed frontend with glowing neon effects, holographic cards, and animated stat bars.

What You'll Build

Type a query into a pulsing cyan search bar and watch Pokemon cards materialize on screen with type-colored borders, official artwork, and relevance scores. Click any card and it expands to reveal animated stat bars that fill with energy.

By the end of this project, you'll have:

  • A glowing search bar that queries your BM25 engine in real-time and displays ranked results as holographic cards.
  • Pokemon cards with sprite images, neon type badges, glass-panel backgrounds, and subtle scanline effects.
  • An expanded detail view with animated stat bars, abilities list, and height/weight data that appears when you click a card.
  • A FastAPI backend that wraps your existing search engine and serves everything from one command.
  • Secret Mission: Add a typing animation to the title and particle effects that respond to searches.

Are there any prerequisites?

This project builds directly on the "Build a Pokedex Search Engine" project. You need engine.py, fetcher.py, and pokedex.json already in your project folder.

If you haven't completed that project yet, finish it first and then come back here.

Before We Start

Before we write any code, let's lock in what you're about to build and why it matters. Your search engine works in the terminal, but wrapping it in a web UI turns it into something you can demo for anyone with a browser.

Set Up FastAPI and the Frontend File Structure

Your Pokedex search engine works, but it lives in the terminal. To let users search Pokemon from a browser, you need a web server that handles HTTP requests and serves frontend files.

In this step, you'll install FastAPI (a Python web framework), create the folder structure for your frontend, and confirm the server responds in your browser.

In this step, get ready to:
  • Install FastAPI as a project dependency.
  • Create a static folder with HTML, CSS, and JavaScript files.
  • Run the development server and confirm it responds with JSON in your browser.
Install FastAPI
  • In VS Code, open the integrated terminal by pressing Ctrl+` (the backtick key, above Tab).
  • Install FastAPI with its standard dependencies by running this command:
uv add "fastapi[standard]"

What does this command do?

uv add installs a Python package and records it in your project's pyproject.toml file.

The [standard] extra bundles useful additions like the development server, which lets you run your app with automatic reload as you make changes.

  • You should see output showing FastAPI and its dependencies being resolved and installed.

Seeing a resolution error?

Make sure your terminal is in the pokedex-search project folder (the one containing pyproject.toml). Run ls to check you can see engine.py and fetcher.py listed.

Help me troubleshoot a uv add error in my pokedex project

Create the frontend file structure

Your frontend needs a dedicated folder to hold the HTML, CSS, and JavaScript files that the server will deliver to the browser.

  • Create the static folder by running this command:
mkdir static
  • In the VS Code Explorer sidebar (left panel), expand the new static folder.
  • Right-click inside the static folder and click New File.
  • Type index.html and press Enter.
  • Right-click inside static again and create a second file named style.css.
  • Create one more file named app.js.
  • Confirm the files exist by running:
ls static
  • You should see app.js, index.html, and style.css in the output.

Not seeing all three files?

If ls static shows fewer than three files, right-click the static folder in VS Code's Explorer again and create the missing one(s). Make sure you're clicking inside static and not the project root.

Help me figure out why my static folder is missing files

Create the server file and launch it

This file is the bridge between your search engine and the browser. It creates a web server that listens for requests and sends back responses.

  • In VS Code's Explorer, right-click in the project root (not inside static) and click New File.
  • Name it server.py and press Enter.
  • Add the following code to server.py:
# Import the FastAPI framework
from fastapi import FastAPI

# Create the app instance
app = FastAPI()

# Define a route for the root URL that returns a JSON greeting
@app.get("/")
async def root():
    return {"message": "Pokedex API is online"}

What does this code do?

  • FastAPI() creates a web application instance that can listen for incoming browser requests.
  • @app.get("/") registers the function below it as the handler for GET requests to the root URL (/).
  • Returning a Python dictionary automatically converts it to a JSON response that the browser can read.
  • Save server.py.

✔️ Awesome, I've got everything!

Great. Make sure you've saved server.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete server.py at this point:

# Import the FastAPI framework
from fastapi import FastAPI

# Create the app instance
app = FastAPI()

# Define a route for the root URL that returns a JSON greeting
@app.get("/")
async def root():
    return {"message": "Pokedex API is online"}

Now it's time to launch the server. This command starts a development server that auto-reloads when you change code. It will take over your terminal until you stop it.

  • Start the FastAPI development server by running:
uv run fastapi dev server.py

What does this command do?

uv run executes a command within your project's virtual environment. fastapi dev starts the development server with auto-reload enabled, so any file changes you save will restart the server automatically.

The server runs at http://127.0.0.1:8000 by default. Your terminal is now occupied by the server process. To stop it later, press Ctrl+C.

  • You should see output indicating the server is running, including the URL http://127.0.0.1:8000.

Before you open the URL, what do you think you'll see in the browser? Remember, your route returns a Python dictionary.

  • Open your browser and navigate to http://127.0.0.1:8000.

You should see a JSON response: {"message": "Pokedex API is online"}. FastAPI automatically converted your Python dictionary into JSON for the browser.

Seeing "connection refused" or a blank page?

  • Check your terminal. If you see a traceback or error instead of the "running" message, you likely have a typo in server.py. Compare your code to the double-check tab above.
  • Make sure you're visiting http://127.0.0.1:8000 (port 8000), not a different port.
  • If the terminal says the address is already in use, another process is on port 8000. Press Ctrl+C and try running the command again.

Help me troubleshoot my FastAPI server not responding

Your server is alive and responding to requests. Next up, you'll connect your search engine to the API so the browser can query Pokemon by name, type, or ability.

Expose the Search Engine as a Web API

Your server is running and responding with a JSON greeting. That proves FastAPI works, but the browser still has no way to query your search engine.

In this step, you will wrap your BM25 search engine in REST API endpoints. Once the engine is exposed over HTTP, any frontend can send a query and receive structured Pokemon data back as JSON.

In this step, get ready to:
  • Connect the existing search engine to your FastAPI server so it loads once at startup.
  • Create a search endpoint that accepts a query and returns ranked Pokemon results.
  • Add a detail endpoint and mount the static files folder for the upcoming frontend.
Connect the search engine to FastAPI

Your search engine lives in engine.py and your data loader lives in fetcher.py. The server needs to import both and build the search index once when it starts up, so every incoming request can query an already-loaded index.

  • In server.py, replace the entire file contents with the following code:
from fastapi import FastAPI

from engine import PokedexSearchEngine
from fetcher import load_pokedex

app = FastAPI()

# Load the Pokedex data and build the search index once at startup
pokedex = load_pokedex()
engine = PokedexSearchEngine(pokedex)


@app.get("/")
async def root():
    return {"message": "Pokedex API is online"}

What does this code do?

The two new imports pull in your existing project code without duplicating anything.

  • load_pokedex() reads pokedex.json and returns the full list of Pokemon dictionaries.
  • PokedexSearchEngine(pokedex) builds the BM25 index from that data. Because this runs at the module level, the index is built once when the server starts, not on every request.
  • Save server.py.
  • Check the terminal where your server is running. It auto-reloads on file changes, so you should see a reload log line with no errors.

Seeing a ModuleNotFoundError?

  • Make sure engine.py, fetcher.py, and pokedex.json are in the same folder as server.py. The imports expect them to be siblings.
  • If you renamed any files in the previous project, update the import statements to match.

Still stuck? Why is my server failing to import engine or fetcher?

Create the search endpoint

The frontend will send search queries as HTTP GET requests with a query parameter. This endpoint receives the query, runs it through the BM25 engine, and returns the ranked results as structured JSON.

  • In server.py, find the @app.get("/") route. Add the following search endpoint above it:
@app.get("/api/search")
async def search(q: str = "", top_k: int = 12):
    # Return empty results if no query provided
    if not q.strip():
        return {"results": [], "query": q}
    # Run the BM25 search and format results
    results = engine.search(q.strip(), top_k=top_k)
    return {
        "query": q,
        "results": [
            {
                "name": pokemon["name"],
                "id": pokemon["id"],
                "types": pokemon["types"],
                "abilities": pokemon["abilities"],
                "stats": pokemon["stats"],
                "height": pokemon["height"],
                "weight": pokemon["weight"],
                "score": round(score, 2),
                "is_fuzzy": is_fuzzy,
            }
            for pokemon, score, is_fuzzy in results
        ],
    }

What does this endpoint do?

  • q: str = "" captures the search query from the URL. If someone visits /api/search?q=fire, the value of q becomes "fire".
  • engine.search() returns a list of 3-tuples: (pokemon_dict, score, is_fuzzy). The list comprehension unpacks each tuple and builds a clean JSON object with only the fields the frontend needs.
  • top_k: int = 12 limits results to 12 by default. The frontend can override this by adding &top_k=20 to the URL.
  • Save server.py and wait for the auto-reload in your terminal.
  • Open your browser and visit:
http://127.0.0.1:8000/api/search?q=fire

You should see a JSON response with several fire-type Pokemon, each showing their name, id, types, stats, and a BM25 score.

Seeing an empty results array?

  • Make sure you typed ?q=fire at the end of the URL. Without the query parameter, the endpoint returns an empty list.
  • Check that pokedex.json exists and is not empty. The search engine needs data to index.

Need help? My /api/search endpoint returns empty results even with a query.

Add the Pokemon detail endpoint and serve static files

The search endpoint returns summary data. A separate detail endpoint lets the frontend fetch full information for a single Pokemon by name. You will also mount the static folder so the browser can load your HTML, CSS, and JavaScript files.

  • In server.py, add the following endpoint below your /api/search function and above the existing root route:
@app.get("/api/pokemon/{name}")
async def get_pokemon(name: str):
    # Find the Pokemon by name in the loaded dataset
    match = next((p for p in pokedex if p["name"] == name.lower()), None)
    if not match:
        return {"error": "Pokemon not found"}
    return match

What does this endpoint do?

The {name} in the URL path becomes a function parameter. Visiting /api/pokemon/charizard sets name to "charizard". The next() call scans the Pokedex list for a matching name, returning the full dictionary or None if no match exists.

Now update the imports and the bottom of the file to serve your static frontend files.

  • At the top of server.py, find the line from fastapi import FastAPI and add these two imports directly below it:
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
  • Find the existing root route at the bottom of the file:
@app.get("/")
async def root():
    return {"message": "Pokedex API is online"}
  • Replace it with the static file mount and the updated root route:
# Mount the static folder so CSS, JS, and images are accessible
app.mount("/static", StaticFiles(directory="static"), name="static")


@app.get("/")
async def root():
    # Serve the frontend HTML as the homepage
    return FileResponse("static/index.html")

Why mount static files this way?

StaticFiles tells FastAPI to serve any file inside the static/ folder at the /static/ URL path. Your CSS will be accessible at /static/style.css, your JavaScript at /static/app.js.

The updated root route uses FileResponse to serve index.html directly at /. This means visitors hit your app at the base URL instead of needing to type /static/index.html.

  • Save server.py and wait for the auto-reload in your terminal.

Before you test, think about this: if you search for fire flying, which Pokemon do you expect to appear at the top of the results?

  • Open your browser and visit:
http://127.0.0.1:8000/api/search?q=fire+flying

You should see a JSON response with Charizard at the top of the results array. Its types include both fire and flying, giving it the highest BM25 relevance score.

✔️ Awesome, I've got everything!

Great. Double check you have saved server.py and that your terminal shows no errors.

ⓧ I'd like to double check the full code

Your server.py should contain these sections in order. Scroll up and confirm each one is present:

  • Imports: FastAPI, StaticFiles, FileResponse, PokedexSearchEngine, and load_pokedex.
  • App creation: app = FastAPI().
  • Startup initialization: pokedex = load_pokedex() and engine = PokedexSearchEngine(pokedex).
  • Search endpoint: @app.get("/api/search") with the full list comprehension returning name, id, types, abilities, stats, height, weight, score, and is_fuzzy.
  • Pokemon detail endpoint: @app.get("/api/pokemon/{name}").
  • Static mount: app.mount("/static", StaticFiles(directory="static"), name="static").
  • Root route: @app.get("/") returning FileResponse("static/index.html").

Server crashing after the changes?

  • If you see an error about the static directory not existing, make sure the static/ folder is in the same directory as server.py.
  • Check that the app.mount line comes after all your @app.get API routes. FastAPI matches routes in order, and the mount can shadow API paths if placed first.
  • Make sure the root route @app.get("/") appears after the mount line. It overrides the mount for the exact / path.

Need help? My FastAPI server crashes after I added StaticFiles or the root route change.

Your search engine is now a fully functional web API. Any client that can make HTTP requests can search your Pokedex and get structured data back. Next up, you will build the frontend that actually makes those requests and displays the results in the browser.

Build a Working but Unstyled Frontend

Your API is live and returning JSON results. Now you need a way for a human to interact with it. A browser-based frontend gives you a search input, a results display, and the ability to show images and styled cards.

In this step, you will build a functional (but plain-looking) single-page frontend that talks to your search API. The goal is to prove the full loop works end-to-end before layering on visual polish in the next step.

In this step, get ready to:
  • Build an HTML page with a search input and results container.
  • Write JavaScript that fetches search results from your API and renders Pokemon cards with sprite images.
  • Verify the full frontend-backend connection by searching for Pokemon.
Build the HTML page structure

Your frontend needs three things: a title, a search input, and an empty container where results will appear. The JavaScript will fill that container dynamically when the user types a query.

  • Open index.html inside your static folder in VS Code.
  • Replace the empty file with the following HTML structure:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Futuristic Pokedex</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <div class="container">
        <header>
            <h1 class="title" id="title">FUTURISTIC POKEDEX</h1>
            <p class="subtitle">BM25-Powered Search Engine</p>
        </header>
        <div class="search-wrapper">
            <input
                type="text"
                id="search-input"
                placeholder="Search Pokemon by name, type, or ability..."
                autocomplete="off"
            >
        </div>
        <div class="results-info" id="results-info"></div>
        <div class="results-grid" id="results-grid"></div>
    </div>
    <script src="/static/app.js"></script>
</body>
</html>

What does this HTML do?

  • The id="search-input" input is where the user types queries. JavaScript will listen for keystrokes on this element.
  • The id="results-grid" div starts empty. JavaScript will inject Pokemon cards here after each search.
  • The style.css and app.js links point to /static/ paths because that is where FastAPI's StaticFiles mount serves them from.
  • Save index.html.
  • Open http://127.0.0.1:8000/static/index.html in your browser.
  • You should see the heading "FUTURISTIC POKEDEX", the subtitle, and a search input on a plain white background.

Seeing a blank page or 404?

  • Make sure your FastAPI dev server is still running in the terminal. If you stopped it, restart with uv run fastapi dev server.py.
  • Confirm the file is saved at static/index.html (not static/static/index.html or in the project root).
  • Try a hard refresh in your browser (Cmd+Shift+R on macOS, Ctrl+Shift+R on Windows) to clear cached responses.

help me debug why my index.html is not loading in the browser

Write the card rendering and search logic

Now you need JavaScript that listens for input in the search box, calls your API, and turns the JSON response into visible cards on the page. Each card will show the Pokemon's name, ID number, types, relevance score, and a sprite image from the PokeAPI sprites repository.

  • Open app.js inside your static folder.
  • Add the following code that sets up DOM references and defines the card-building function:
const searchInput = document.getElementById('search-input');
const resultsGrid = document.getElementById('results-grid');
const resultsInfo = document.getElementById('results-info');

const SPRITE_BASE = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/';

let debounceTimer = null;

function createCard(pokemon) {
    const card = document.createElement('div');
    card.classList.add('pokemon-card');
    const typeBadges = pokemon.types.map(t => `<span class="type-badge">${t}</span>`).join('');
    const fuzzyTag = pokemon.is_fuzzy ? ' [fuzzy]' : '';
    card.innerHTML = `
        <div class="card-header">
            <img class="card-sprite" src="${SPRITE_BASE}${pokemon.id}.png" alt="${pokemon.name}" loading="lazy">
            <div class="card-info">
                <div class="card-name">${pokemon.name}</div>
                <div class="card-id">#${String(pokemon.id).padStart(4, '0')}</div>
                <div class="card-types">${typeBadges}</div>
            </div>
        </div>
        <div class="card-score">Score: ${pokemon.score}${fuzzyTag}</div>
    `;
    return card;
}

How do the sprite images work?

The SPRITE_BASE constant points to the PokeAPI sprites GitHub repository. Each Pokemon's official artwork is stored at a predictable URL ending in {id}.png.

For example, Charizard (ID 6) loads from ...official-artwork/6.png. The loading="lazy" attribute tells the browser to only fetch images when they scroll into view, keeping initial load fast.

  • Below the createCard function, add the search logic and event listener:
async function performSearch(query) {
    if (!query.trim()) {
        resultsGrid.innerHTML = '';
        resultsInfo.textContent = '';
        return;
    }
    try {
        const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
        const data = await response.json();
        if (data.results.length === 0) {
            resultsGrid.innerHTML = '';
            resultsInfo.textContent = 'No Pokemon found.';
            return;
        }
        resultsInfo.textContent = `Found ${data.results.length} result(s)`;
        resultsGrid.innerHTML = '';
        data.results.forEach(pokemon => {
            resultsGrid.appendChild(createCard(pokemon));
        });
    } catch (error) {
        resultsInfo.textContent = 'Search failed. Is the server running?';
    }
}

searchInput.addEventListener('input', (e) => {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
        performSearch(e.target.value);
    }, 300);
});

What does this search logic do?

  • performSearch calls your /api/search endpoint with the user's query, then loops over the results and appends a card for each Pokemon.
  • The event listener fires on every keystroke, but debounceTimer waits 300ms of silence before actually calling the API. This prevents flooding your server with a request for every single letter typed.
  • If the query is empty, it clears the results instead of sending a wasted request.
  • Save app.js.
Test the frontend-backend connection

Both files are in place. Time to see if the frontend can talk to your API and render results.

  • Switch to your browser and refresh http://127.0.0.1:8000/static/index.html.

Before you type a search, think about what you expect to happen. Will the results appear instantly, or after a brief delay? Why?

  • Type electric into the search input.

After a brief 300ms pause, you should see a list of Pokemon names appear below the input. Each card shows a name (like "pikachu"), an ID number, type badges, a relevance score, and an official artwork image. It all works but looks completely unstyled.

No results appearing when you type?

  • Open your browser's developer console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows). Look for red error messages. A 404 on /api/search means the server is not running. A TypeError means the element IDs in your HTML do not match the JavaScript.
  • Make sure the id values in your HTML (search-input, results-grid, results-info) match exactly what app.js uses in getElementById.
  • If sprites are not loading but names appear, check your internet connection. The images load from GitHub's CDN.

help me debug why my Pokemon search results are not appearing in the browser

✔️ Awesome, I've got everything!

Great. Double check you have saved both index.html and app.js in the static folder.

ⓧ I'd like to double check the full code

Here is the complete index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Futuristic Pokedex</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <div class="container">
        <header>
            <h1 class="title" id="title">FUTURISTIC POKEDEX</h1>
            <p class="subtitle">BM25-Powered Search Engine</p>
        </header>
        <div class="search-wrapper">
            <input
                type="text"
                id="search-input"
                placeholder="Search Pokemon by name, type, or ability..."
                autocomplete="off"
            >
        </div>
        <div class="results-info" id="results-info"></div>
        <div class="results-grid" id="results-grid"></div>
    </div>
    <script src="/static/app.js"></script>
</body>
</html>

Here is the complete app.js. Confirm these sections are present in order:

  • DOM references (searchInput, resultsGrid, resultsInfo) and the SPRITE_BASE constant.
  • The createCard function that builds a card div with sprite, name, ID, types, and score.
  • The performSearch function that fetches from /api/search and renders results.
  • The input event listener with a 300ms debounce.

Your frontend is functional. You can search Pokemon and see real results with images. But it looks like a plain homework page. Next up, you will transform it into a futuristic sci-fi interface with glowing neon effects and glassmorphism.

Apply the Futuristic Sci-Fi Theme

Your Pokedex works. You can type a query, hit the API, and see results. But right now it looks like a homework assignment.

In this step, you'll transform the plain page into something that looks like it was pulled from a sci-fi movie. You'll learn advanced CSS techniques like layered box-shadows for neon glow, backdrop-filter for glassmorphism, and CSS custom properties for a consistent color system.

In this step, get ready to:
  • Set up the dark theme foundation with custom color properties and a grid-patterned background.
  • Style the search bar with a glowing cyan border and pulsing focus animation.
  • Style result cards with glassmorphism, type-colored borders, and neon hover effects.
Set up the dark theme foundation

The futuristic look starts with two things: loading the right fonts and defining a palette of neon colors as CSS custom properties. Custom properties let you define a color once and reuse it everywhere.

  • Open static/index.html in VS Code.
  • Replace the entire file contents with this updated HTML that adds Google Fonts links, a scanlines overlay div, and a search-glow element:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Futuristic Pokedex</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Exo+2:wght@300;400;600&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <div class="scanlines"></div>
    <div class="container">
        <header>
            <h1 class="title" id="title">FUTURISTIC POKEDEX</h1>
            <p class="subtitle">BM25-Powered Search Engine</p>
        </header>
        <div class="search-wrapper">
            <input
                type="text"
                id="search-input"
                placeholder="Search Pokemon by name, type, or ability..."
                autocomplete="off"
            >
            <div class="search-glow"></div>
        </div>
        <div class="results-info" id="results-info"></div>
        <div class="results-grid" id="results-grid"></div>
    </div>
    <script src="/static/app.js"></script>
</body>
</html>

What changed in the HTML?

  • Three new link tags in the <head> load Orbitron (a geometric sci-fi font for headings) and Exo 2 (a clean sans-serif for body text) from Google Fonts.
  • A <div class="scanlines"> sits before the container. CSS will turn it into a subtle CRT-monitor scanline overlay.
  • A <div class="search-glow"> inside the search wrapper will become an animated gradient line that appears on focus.
  • Save static/index.html.
  • Now open static/style.css and add the CSS custom properties that define your color palette:
:root {
    --bg-primary: #0a0e17;
    --bg-card: rgba(15, 23, 42, 0.7);
    --text-primary: #e0e8ff;
    --text-muted: #8892b0;
    --accent-cyan: #00f0ff;
    --accent-magenta: #ff00aa;
    --accent-green: #39ff14;
    --glow-cyan: 0 0 10px rgba(0, 240, 255, 0.3), 0 0 30px rgba(0, 240, 255, 0.15), 0 0 60px rgba(0, 240, 255, 0.05);
    --type-normal: #a8a878;
    --type-fire: #f08030;
    --type-water: #6890f0;
    --type-electric: #f8d030;
    --type-grass: #78c850;
    --type-ice: #98d8d8;
    --type-fighting: #c03028;
    --type-poison: #a040a0;
    --type-ground: #e0c068;
    --type-flying: #a890f0;
    --type-psychic: #f85888;
    --type-bug: #a8b820;
    --type-rock: #b8a038;
    --type-ghost: #705898;
    --type-dragon: #7038f8;
    --type-dark: #705848;
    --type-steel: #b8b8d0;
    --type-fairy: #ee99ac;
}

What are CSS custom properties?

Custom properties (variables) defined in :root are available everywhere in your stylesheet. Instead of repeating #00f0ff in 20 places, you write var(--accent-cyan) and change the color in one spot if needed.

The --glow-cyan variable holds a layered box-shadow value with three levels of diffusion. This creates the soft neon glow effect you'll see on the search bar.

  • Below the :root block, add the base styles that apply the dark background, grid pattern, and heading fonts:
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
    font-family: 'Exo 2', sans-serif;
    background-color: var(--bg-primary);
    color: var(--text-primary);
    min-height: 100vh;
    background-image:
        linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px);
    background-size: 50px 50px;
}

.scanlines {
    position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    pointer-events: none; z-index: 1000;
    background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px);
}

.container { max-width: 1100px; margin: 0 auto; padding: 3rem 1.5rem; position: relative; z-index: 1; }

header { text-align: center; margin-bottom: 2.5rem; }

.title {
    font-family: 'Orbitron', sans-serif; font-size: 2.5rem; font-weight: 900;
    color: var(--accent-cyan);
    text-shadow: 0 0 10px rgba(0,240,255,0.5), 0 0 30px rgba(0,240,255,0.3);
    letter-spacing: 2px; min-height: 3.5rem;
}

.subtitle { font-size: 0.9rem; color: var(--text-muted); margin-top: 0.5rem; letter-spacing: 3px; text-transform: uppercase; }

What does this code do?

  • The body uses two overlapping linear-gradient backgrounds to create a faint cyan grid pattern over the dark base color.
  • The .scanlines div uses a repeating gradient to draw transparent-then-dark stripes every 4px, mimicking a CRT monitor.
  • The .title applies Orbitron with layered text-shadow to create the neon glow behind the heading text.
  • Save static/style.css and refresh your browser at http://127.0.0.1:8000.
  • You should see a dark background with a faint cyan grid pattern, and the title "FUTURISTIC POKEDEX" rendered in a geometric font with a glowing cyan halo.

Still seeing a white page?

  • Make sure you saved both index.html and style.css. The font links in HTML must load before the CSS can reference them.
  • Check that the :root block ends with a closing } before the * reset rule. A missing brace breaks the entire stylesheet.
  • Open DevTools (F12) and check the Console tab for errors loading the CSS file. Help me debug my CSS not loading.
Style the search bar with a glowing border

The search bar is the main interaction point. A glass-panel background with a glowing border on focus makes it feel like a sci-fi terminal input.

  • In static/style.css, add these search bar styles below the .subtitle rule:
.search-wrapper { position: relative; max-width: 600px; margin: 0 auto 2rem; }

#search-input {
    width: 100%; padding: 1rem 1.5rem; font-size: 1rem; font-family: 'Exo 2', sans-serif;
    background: rgba(15,23,42,0.8); border: 1px solid rgba(0,240,255,0.3); border-radius: 8px;
    color: var(--text-primary); outline: none; backdrop-filter: blur(10px);
    transition: border-color 0.3s, box-shadow 0.3s;
}

#search-input:focus { border-color: var(--accent-cyan); box-shadow: var(--glow-cyan); }
#search-input::placeholder { color: var(--text-muted); }

.search-glow {
    position: absolute; bottom: -2px; left: 50%; transform: translateX(-50%);
    width: 60%; height: 2px;
    background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
    opacity: 0; transition: opacity 0.3s;
}
#search-input:focus ~ .search-glow { opacity: 1; }

.results-info { text-align: center; color: var(--text-muted); font-size: 0.85rem; margin-bottom: 1.5rem; min-height: 1.2rem; }

.results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.25rem; }

What does this code do?

  • The input uses backdrop-filter: blur(10px) to blur the background behind it, creating a frosted-glass effect called glassmorphism.
  • On focus, box-shadow: var(--glow-cyan) applies three layered shadows at increasing blur radii. This creates a soft, radiating neon glow.
  • The .search-glow div is a 2px gradient line that fades in below the input on focus using the ~ sibling selector.
  • The .results-grid uses CSS Grid with auto-fill to responsively arrange cards in columns that are at least 300px wide.
  • Save static/style.css and refresh your browser.
  • Click into the search bar. You should see a glowing cyan border pulse around the input and a faint gradient line appear below it.

No glow on focus?

  • Make sure you have the .search-glow div inside your HTML's .search-wrapper, right after the <input>. The CSS ~ selector requires it to be a sibling that comes after the input.
  • Check that backdrop-filter is supported in your browser. It works in Chrome, Edge, and Firefox. Safari requires the -webkit- prefix.
  • Help me debug why my search bar glow isn't showing.
Style result cards with glassmorphism and type colors

The cards need to look like holographic panels. Each card gets a blurred glass background, a left border colored by the Pokemon's primary type, and neon badges that glow with their type color.

For the CSS to color each card by type, the JavaScript needs to pass type color information through CSS custom properties on each card element. You'll update app.js first, then add the card styles.

  • Open static/app.js and find this line near the top:
const SPRITE_BASE = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/';
  • Add this TYPE_COLORS map directly below it. This maps each Pokemon type to a hex color that the CSS will use for borders and badges:
// Map each Pokemon type to a neon color for card borders and badges
const TYPE_COLORS = {
    normal: '#a8a878', fire: '#f08030', water: '#6890f0', electric: '#f8d030',
    grass: '#78c850', ice: '#98d8d8', fighting: '#c03028', poison: '#a040a0',
    ground: '#e0c068', flying: '#a890f0', psychic: '#f85888', bug: '#a8b820',
    rock: '#b8a038', ghost: '#705898', dragon: '#7038f8', dark: '#705848',
    steel: '#b8b8d0', fairy: '#ee99ac',
};
  • Now replace the entire createCard function with this updated version that sets a --type-color CSS variable on each card and colors the type badges:
function createCard(pokemon) {
    // Use the first type's color for the card border
    const typeColor = TYPE_COLORS[pokemon.types[0]] || '#00f0ff';
    const card = document.createElement('div');
    card.classList.add('pokemon-card');
    card.style.setProperty('--type-color', typeColor);
    // Build colored type badges with glow
    const typeBadges = pokemon.types.map(t => {
        const color = TYPE_COLORS[t] || '#555';
        return `<span class="type-badge" style="--badge-color: ${color}; background: ${color};">${t}</span>`;
    }).join('');
    const fuzzyTag = pokemon.is_fuzzy ? ' <span class="fuzzy-tag">[fuzzy]</span>' : '';
    card.innerHTML = `
        <div class="card-header">
            <img class="card-sprite" src="${SPRITE_BASE}${pokemon.id}.png" alt="${pokemon.name}" loading="lazy">
            <div class="card-info">
                <div class="card-name">${pokemon.name}</div>
                <div class="card-id">#${String(pokemon.id).padStart(4, '0')}</div>
                <div class="card-types">${typeBadges}</div>
            </div>
        </div>
        <div class="card-score">Score: ${pokemon.score}${fuzzyTag}</div>
    `;
    return card;
}

What changed in createCard?

  • Each card gets a --type-color CSS custom property set via JavaScript. The CSS uses this for the left border color.
  • Each type badge now has inline --badge-color and background styles so the CSS can add a matching glow via box-shadow.
  • The fuzzy tag is now wrapped in a <span class="fuzzy-tag"> so CSS can style it with an accent color.
  • Save static/app.js.
  • Now switch back to static/style.css and add the card styles below the .results-grid rule:
.pokemon-card {
    background: var(--bg-card); border: 1px solid rgba(255,255,255,0.05); border-radius: 12px;
    padding: 1.25rem; backdrop-filter: blur(10px); cursor: pointer;
    transition: transform 0.2s, box-shadow 0.3s, border-color 0.3s;
    border-left: 3px solid var(--type-color, var(--accent-cyan));
}
.pokemon-card:hover { transform: translateY(-3px); box-shadow: 0 8px 30px rgba(0,240,255,0.1); border-color: rgba(255,255,255,0.1); }

.card-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem; }
.card-sprite { width: 72px; height: 72px; object-fit: contain; filter: drop-shadow(0 0 6px rgba(0,240,255,0.3)); }
.card-info { flex: 1; }
.card-name { font-family: 'Orbitron', sans-serif; font-size: 1rem; font-weight: 700; text-transform: capitalize; }
.card-id { font-size: 0.8rem; color: var(--text-muted); }
.card-types { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
.type-badge {
    font-size: 0.7rem; font-weight: 600; padding: 0.2rem 0.6rem; border-radius: 4px;
    text-transform: uppercase; letter-spacing: 1px; color: #fff;
    background: var(--badge-color, #555); box-shadow: 0 0 8px var(--badge-color, transparent);
}
.card-score { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.5rem; }
.card-score .fuzzy-tag { color: var(--accent-magenta); font-style: italic; }

What does this code do?

  • Each .pokemon-card uses backdrop-filter: blur(10px) on a semi-transparent background to create the frosted-glass look.
  • The border-left reads var(--type-color) that JavaScript set, so fire-type Pokemon get an orange border and water-types get blue.
  • On hover, transform: translateY(-3px) lifts the card slightly while a box-shadow creates a soft glow underneath.
  • Each .type-badge uses box-shadow: 0 0 8px with its own color to emit a faint neon glow.
  • Save static/style.css.

Before you refresh, what do you think the cards will look like now that each one has a type-colored left border and backdrop blur?

  • Refresh your browser and type fire flying in the search bar.

You should see translucent glass cards with an orange left border (fire type), neon-glowing type badges, official Pokemon artwork, and a subtle lift effect when you hover over each card.

Cards look unstyled or have no borders?

  • Open DevTools and inspect a card element. Check that it has a --type-color property in its inline styles. If not, make sure you saved app.js after updating createCard.
  • Verify that TYPE_COLORS is defined before the createCard function. JavaScript reads top-to-bottom, so the constant must exist before it's used.
  • Help me debug why my Pokemon cards aren't showing type colors.

✔️ Awesome, I've got everything!

Great. Double check you've saved all three files before moving on.

ⓧ I'd like to double check the full code

Here is the full static/style.css at this point. Scroll up and confirm each section is present in order: :root variables, base styles (body, scanlines, container, header, title, subtitle), search styles, and card styles.

Here is the full static/app.js:

const searchInput = document.getElementById('search-input');
const resultsGrid = document.getElementById('results-grid');
const resultsInfo = document.getElementById('results-info');

const SPRITE_BASE = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/';

const TYPE_COLORS = {
    normal: '#a8a878', fire: '#f08030', water: '#6890f0', electric: '#f8d030',
    grass: '#78c850', ice: '#98d8d8', fighting: '#c03028', poison: '#a040a0',
    ground: '#e0c068', flying: '#a890f0', psychic: '#f85888', bug: '#a8b820',
    rock: '#b8a038', ghost: '#705898', dragon: '#7038f8', dark: '#705848',
    steel: '#b8b8d0', fairy: '#ee99ac',
};

let debounceTimer = null;
function createCard(pokemon) {
    const typeColor = TYPE_COLORS[pokemon.types[0]] || '#00f0ff';
    const card = document.createElement('div');
    card.classList.add('pokemon-card');
    card.style.setProperty('--type-color', typeColor);
    const typeBadges = pokemon.types.map(t => {
        const color = TYPE_COLORS[t] || '#555';
        return `<span class="type-badge" style="--badge-color: ${color}; background: ${color};">${t}</span>`;
    }).join('');
    const fuzzyTag = pokemon.is_fuzzy ? ' <span class="fuzzy-tag">[fuzzy]</span>' : '';
    card.innerHTML = `
        <div class="card-header">
            <img class="card-sprite" src="${SPRITE_BASE}${pokemon.id}.png" alt="${pokemon.name}" loading="lazy">
            <div class="card-info">
                <div class="card-name">${pokemon.name}</div>
                <div class="card-id">#${String(pokemon.id).padStart(4, '0')}</div>
                <div class="card-types">${typeBadges}</div>
            </div>
        </div>
        <div class="card-score">Score: ${pokemon.score}${fuzzyTag}</div>
    `;
    return card;
}
async function performSearch(query) {
    if (!query.trim()) { resultsGrid.innerHTML = ''; resultsInfo.textContent = ''; return; }
    try {
        const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
        const data = await response.json();
        if (data.results.length === 0) { resultsGrid.innerHTML = ''; resultsInfo.textContent = 'No Pokemon found.'; return; }
        resultsInfo.textContent = `Found ${data.results.length} result(s)`;
        resultsGrid.innerHTML = '';
        data.results.forEach(pokemon => { resultsGrid.appendChild(createCard(pokemon)); });
    } catch (error) {
        resultsInfo.textContent = 'Search failed. Is the server running?';
    }
}

searchInput.addEventListener('input', (e) => {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => { performSearch(e.target.value); }, 300);
});

And here is the full static/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Futuristic Pokedex</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Exo+2:wght@300;400;600&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <div class="scanlines"></div>
    <div class="container">
        <header>
            <h1 class="title" id="title">FUTURISTIC POKEDEX</h1>
            <p class="subtitle">BM25-Powered Search Engine</p>
        </header>
        <div class="search-wrapper">
            <input
                type="text"
                id="search-input"
                placeholder="Search Pokemon by name, type, or ability..."
                autocomplete="off"
            >
            <div class="search-glow"></div>
        </div>
        <div class="results-info" id="results-info"></div>
        <div class="results-grid" id="results-grid"></div>
    </div>
    <script src="/static/app.js"></script>
</body>
</html>

Your Pokedex now looks like something from a sci-fi cockpit. Next up, you'll add a click-to-expand detail view with animated stat bars that fill with energy when a card opens.

Add the Pokemon Detail Expansion

Your cards show names, types, and artwork. That's enough to identify a Pokemon, but a real Pokedex reveals the full picture: stats, abilities, and physical measurements.

In this step, you'll add a click-to-expand detail view under each card. Clicking a card reveals six animated stat bars that fill from zero to their values, plus abilities and height/weight. This teaches DOM manipulation, CSS transitions for smooth reveals, and animated bar charts.

In this step, get ready to:
  • Add stat bar helper functions to build and animate the bars.
  • Update createCard with a detail section and click handler.
  • Style the expansion with holographic scanline effects and animated fills.
Add the stat bar helper functions

Each Pokemon has six stats (HP, Attack, Defense, Sp. Atk, Sp. Def, Speed) stored as numbers from 1 to 255. You need two functions: one to build the bar HTML, and one to trigger the animation after the section expands.

  • In app.js, add the following two functions below your existing createCard function:
function buildStatBars(stats) {
    // Map API stat names to short display labels
    const statNames = { 'hp': 'HP', 'attack': 'ATK', 'defense': 'DEF', 'special-attack': 'SP.ATK', 'special-defense': 'SP.DEF', 'speed': 'SPD' };
    return Object.entries(stats).map(([name, value]) => {
        const label = statNames[name] || name.toUpperCase();
        // Each bar's fill width is a percentage of 255 (the max possible stat)
        return `<div class="stat-bar-container"><div class="stat-label"><span>${label}</span><span>${value}</span></div><div class="stat-bar"><div class="stat-fill" data-width="${(value / 255) * 100}"></div></div></div>`;
    }).join('');
}

function animateStatBars(container) {
    // Set each fill's width to trigger the CSS transition
    const fills = container.querySelectorAll('.stat-fill');
    fills.forEach(fill => { fill.style.width = fill.getAttribute('data-width') + '%'; });
}

What do these functions do?

The buildStatBars function converts the stats object into HTML. Each stat gets a bar whose data-width attribute stores the target fill percentage (value divided by 255, the maximum stat).

The animateStatBars function reads each bar's data-width and sets it as the actual CSS width. Because the bars start at 0% width and CSS transitions handle the rest, this one line triggers the smooth fill animation.

  • Save app.js. The next chunk puts these helpers to work inside createCard.
Update createCard with the detail expansion

Now you'll add the detail section HTML inside each card's template, plus a click handler that toggles it open and closed.

  • In app.js, find this line inside your createCard function:
        <div class="card-score">Score: ${pokemon.score}${fuzzyTag}</div>
    `;
    return card;
  • Replace those three lines with the following code, which adds the detail section HTML after the score and a click handler before returning the card:
        <div class="card-score">Score: ${pokemon.score}${fuzzyTag}</div>
        <div class="card-details" id="details-${pokemon.id}">
            <div class="details-content">
                <div class="detail-row">
                    <span>Abilities: ${pokemon.abilities.map(a => a.replace('-', ' ')).join(', ')}</span>
                </div>
                <div class="detail-row">
                    <span>Height: ${(pokemon.height / 10).toFixed(1)}m</span>
                    <span>Weight: ${(pokemon.weight / 10).toFixed(1)}kg</span>
                </div>
                ${buildStatBars(pokemon.stats)}
            </div>
        </div>
    `;
    // Toggle detail expansion on card click
    card.addEventListener('click', () => {
        const details = card.querySelector('.card-details');
        const isExpanding = !details.classList.contains('expanded');
        details.classList.toggle('expanded');
        if (isExpanding) { setTimeout(() => animateStatBars(details), 50); }
    });
    return card;

What does this code do?

The card-details div contains abilities, height/weight, and the stat bars built by buildStatBars(). It starts hidden (once CSS is added) and reveals on click.

The click handler toggles the expanded class. When expanding, it waits 50ms before calling animateStatBars() so the CSS transition has time to start before the fill width changes.

  • Save app.js and refresh your browser at http://127.0.0.1:8000.
  • Type fire in the search bar.

You should see abilities, height/weight, and stat labels appearing below each card. They're visible all the time right now because the CSS to hide them isn't in place yet. That's expected.

Seeing a blank page or no results?

Open your browser's DevTools console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows). If you see buildStatBars is not defined, make sure you added the two helper functions below createCard and saved the file.

If you see a syntax error, check that the backtick closing the template literal (`;) comes after the card-details closing div, not before it.

Still stuck? Help me debug why my card details aren't showing up after I updated createCard.

Style the expansion with holographic effects

The detail section is visible but unstyled. Now you'll add CSS that hides it by default and smoothly expands on click, with a holographic scanline overlay and animated gradient-filled stat bars.

  • In style.css, add the following styles at the very bottom of the file:
/* Detail expansion - hidden by default, slides open on click */
.card-details {
    max-height: 0; overflow: hidden;
    transition: max-height 0.4s ease, padding 0.4s ease;
    border-top: 1px solid transparent;
}
.card-details.expanded {
    max-height: 400px; padding-top: 1rem; margin-top: 0.75rem;
    border-top-color: rgba(255,255,255,0.05);
}

/* Holographic scanline overlay on detail content */
.details-content { position: relative; }
.details-content::before {
    content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%;
    pointer-events: none;
    background: repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(0,240,255,0.02) 3px, rgba(0,240,255,0.02) 6px);
    border-radius: 8px;
}

What does this CSS do?

The max-height: 0 trick hides the details by collapsing them to zero height. When the expanded class is added, max-height jumps to 400px and the CSS transition animates the reveal smoothly.

The ::before pseudo-element layers a repeating-linear-gradient of faint cyan lines over the content. This creates the holographic scanline effect without blocking clicks.

  • Still in style.css, add the stat bar styles directly below the code you just pasted:
/* Stat bar layout and labels */
.detail-row { display: flex; justify-content: space-between; font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.75rem; }
.stat-bar-container { margin-bottom: 0.5rem; }
.stat-label { font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 0.2rem; display: flex; justify-content: space-between; }
.stat-bar { height: 6px; background: rgba(255,255,255,0.05); border-radius: 3px; overflow: hidden; }

/* Animated gradient fill - starts at 0% and transitions to target width */
.stat-fill {
    height: 100%; border-radius: 3px;
    background: linear-gradient(90deg, var(--accent-cyan), var(--accent-magenta));
    width: 0%; transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
    box-shadow: 0 0 6px rgba(0,240,255,0.4);
}

Why start at 0% width?

The .stat-fill starts at width: 0% in CSS. When JavaScript sets the actual width (via animateStatBars), the transition property kicks in and smoothly animates from zero to the target value. The cubic-bezier easing gives it a natural deceleration feel.

  • Save style.css and refresh your browser.

Before you click a card, what do you expect to see happen to the details section?

  • Search for fire again, then click any Pokemon card.

The card expands smoothly to reveal six stat bars that animate from zero to their values with a cyan-to-magenta gradient fill. You'll also see abilities and height/weight data, all overlaid with a faint holographic scanline texture.

Card not expanding when clicked?

If clicking does nothing visible, check that your CSS has both .card-details (with max-height: 0) and .card-details.expanded (with max-height: 400px). Without both rules, the toggle has no visible effect.

If the bars appear but don't animate, confirm .stat-fill has width: 0% and transition: width 0.8s in your CSS. The animation relies on transitioning from 0% to the target.

Still stuck? Help me debug why clicking a Pokemon card doesn't expand the detail section with stat bars.

✔️ Awesome, I've got everything!

Great. Double check you've saved both app.js and style.css.

ⓧ I'd like to double check the full code

Scroll up and confirm each chunk is present in order:

  • In app.js: the buildStatBars() and animateStatBars() functions below createCard.
  • In app.js: the card-details div added to createCard's innerHTML (after the card-score div), plus the click handler before return card.
  • In style.css: the .card-details, .card-details.expanded, .details-content, and .details-content::before rules.
  • In style.css: the .detail-row, .stat-bar-container, .stat-label, .stat-bar, and .stat-fill rules.

Your Pokedex cards now reveal full detail panels with animated stat bars, abilities, and physical measurements. Next up, you'll tackle the Secret Mission: adding a typing animation to the title and particle effects that respond to searches.

Secret mission

Add a Typing Animation and Search Particles

Your Pokedex looks futuristic, but it loads like a static document. Real sci-fi interfaces feel alive. They boot up, pulse, and react to input. In this mission, you will make your title type itself out like a system coming online, and make glowing particles burst from the search bar every time a query returns results.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The FastAPI dev server (running process in your terminal).
  • The server.py file (web API for your search engine).
  • The static/ folder containing index.html, style.css, and app.js (frontend files).
  • The FastAPI package dependency (added to your project's virtual environment).

Keep everything running

No action needed. Choose this if you are still actively building or want to experiment with the Pokedex UI further.

  • Your dev server stops automatically when you close the terminal window.
  • To restart it later, open a terminal in your pokedex-search folder and run uv run fastapi dev server.py.

Pause - I'll come back to this later

Shut down running processes to free up your terminal. Your files stay on disk for when you return.

  • Switch to the terminal running the dev server.
  • Press Ctrl+C to stop the FastAPI server.
  • Close the terminal window.
  • Your project files remain in the pokedex-search folder on your Desktop. Restart anytime with uv run fastapi dev server.py.

Delete - I don't want to use this again

Remove all project files created during this project and the prerequisite. This cannot be undone.

  • Switch to the terminal running the dev server and press Ctrl+C to stop it.
  • Remove the entire project folder by running these commands:
cd ~/Desktop
rm -rf pokedex-search

On Windows, run this instead:

cd ~\Desktop
Remove-Item -Recurse -Force pokedex-search
  • Confirm the folder is gone by running:
ls
  • You should no longer see pokedex-search in the output.

What about the FastAPI package?

FastAPI was installed inside your project's virtual environment (the .venv folder inside pokedex-search). Deleting the project folder removes the virtual environment and all its packages automatically.

Nice Work!

Nice Work!

Nice work! You just transformed a terminal-only search engine into a visually stunning web application that looks like it was pulled straight from a sci-fi movie.

Here's what you learned:

  • Built a FastAPI backend that wraps your BM25 search engine in clean REST endpoints, turning a Python script into a service any frontend can consume.
  • Created a futuristic single-page frontend with advanced CSS techniques including neon glow via layered box-shadows, glassmorphism with backdrop-filter, animated stat bars, scanline overlays, and type-colored accents.
  • Connected frontend to backend with the Fetch API, rendering live search results as holographic cards with Pokemon artwork and expandable detail views.
  • Secret Mission: Added a typing animation for the title and particle burst effects on search to make the UI feel alive and responsive.

Ready to quiz yourself?