Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/components/PokemonList/PokemonList.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
.pokemon-list {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
max-width: 300px;
max-height: 500px;
overflow-y: auto;
background: #f8f9fa;
padding: 12px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}

.pokemon-list-empty {
padding: 24px;
text-align: center;
color: #666;
font-family: sans-serif;
}

.pokemon-list-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: #fff;
border: 1px solid #ddd;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
font-family: sans-serif;
}

.pokemon-list-item:hover {
background: #f0f0f0;
transform: translateY(-1px);
}

.pokemon-list-item.selected {
border-color: #007bff;
background: #e6f2ff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.pokemon-list-item img {
width: 40px;
height: 40px;
object-fit: contain;
}

.pokemon-id {
font-size: 12px;
color: #888;
font-weight: bold;
min-width: 35px;
}

.pokemon-name {
font-size: 14px;
font-weight: 500;
color: #333;
text-transform: capitalize;
}
44 changes: 44 additions & 0 deletions src/components/PokemonList/PokemonList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, test, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { PokemonList } from "./PokemonList";

const mockPokemons = [
{ id: 25, name: "pikachu", sprite: "pikachu.png" },
{ id: 4, name: "charmander", sprite: "charmander.png" },
{ id: 1, name: "bulbasaur", sprite: "bulbasaur.png" },
];

describe("PokemonList Component", () => {
test("renders sorted list of pokemon", () => {
render(<PokemonList pokemons={mockPokemons} onSelect={() => {}} />);

const items = screen.getAllByRole("button");
expect(items).toHaveLength(3);

// Verifies sorting by ID (1, 4, 25)
expect(items[0].textContent).toContain("bulbasaur");
expect(items[1].textContent).toContain("charmander");
expect(items[2].textContent).toContain("pikachu");
});

test("calls onSelect with correct ID when clicked", () => {
const handleSelect = vi.fn();
render(<PokemonList pokemons={mockPokemons} onSelect={handleSelect} />);

const pikachuButton = screen.getByText("pikachu");
fireEvent.click(pikachuButton);

expect(handleSelect).toHaveBeenCalledWith(25);
});

test("matches snapshot", () => {
const { asFragment } = render(
<PokemonList
pokemons={mockPokemons}
onSelect={() => {}}
selectedId={4}
/>,
);
expect(asFragment()).toMatchSnapshot();
});
});
54 changes: 54 additions & 0 deletions src/components/PokemonList/PokemonList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import "./PokemonList.css";

export type ListPokemon = {
id: number;
name: string;
sprite: string;
};

type PokemonListProps = {
pokemons: ListPokemon[];
onSelect: (id: number) => void;
selectedId?: number;
};

export function PokemonList({
pokemons,
onSelect,
selectedId,
}: PokemonListProps) {
// Ensure the list is always sorted by ID as required
const sortedPokemons = [...pokemons].sort((a, b) => a.id - b.id);

if (sortedPokemons.length === 0) {
return (
<div className="pokemon-list-empty">No Pokémon available to display.</div>
);
}

return (
<div className="pokemon-list">
{sortedPokemons.map((pokemon) => {
const isSelected = pokemon.id === selectedId;
return (
<button
key={pokemon.id}
className={`pokemon-list-item ${isSelected ? "selected" : ""}`}
onClick={() => onSelect(pokemon.id)}
aria-pressed={isSelected}
>
<span className="pokemon-id">
#{String(pokemon.id).padStart(3, "0")}
</span>
<img
src={pokemon.sprite}
alt={`${pokemon.name} sprite`}
loading="lazy"
/>
<span className="pokemon-name">{pokemon.name}</span>
</button>
);
})}
</div>
);
}