Skip to content
27 changes: 24 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { getPokemonByID } from "./api/pokemon";
import { PokemonCard } from "./components/PokemonCard";
import { PokemonCard } from "./components/PokemonCard/PokemonCard";
import { Favorite } from "./components/Favorite/Favorite";

export default function App() {
const [pokemonId, setPokemonId] = useState(Math.ceil(Math.random() * 493));
// Start at a default ID
const [pokemonId, setPokemonId] = useState(1);

const query = useQuery({
queryKey: ["pokemon", pokemonId],
Expand All @@ -19,6 +21,14 @@ export default function App() {
setPokemonId((prev) => prev + 1);
};

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value, 10);
// Ensure the user cannot enter negative numbers or invalid text
if (!isNaN(value) && value > 0) {
setPokemonId(value);
}
};

return (
<div
style={{
Expand All @@ -28,16 +38,27 @@ export default function App() {
gap: "16px",
}}
>
<div style={{ display: "flex", gap: "10px" }}>
<div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
<button onClick={handlePrevious} disabled={pokemonId <= 1}>
&larr; Prev
</button>

<input
type="number"
value={pokemonId}
onChange={handleInputChange}
min="1"
style={{ width: "70px", textAlign: "center", padding: "4px" }}
aria-label="Pokemon ID"
/>

<button onClick={handleNext}>Next &rarr;</button>
</div>

{query.isPending && <h1>Loading...</h1>}
{query.isError && <h1>Failed to load Pokemon</h1>}
{query.data && <PokemonCard pokemon={query.data} />}
<Favorite pokemonId={pokemonId} />
</div>
);
}
25 changes: 25 additions & 0 deletions src/components/Favorite/Favorite.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.favorite-button {
background: transparent;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease-in-out;
}

.favorite-button:hover {
transform: scale(1.2);
}

.favorite-button:focus-visible {
outline: 2px solid #0056b3;
outline-offset: 2px;
}

.favorite-button:active {
transform: scale(0.9);
}
46 changes: 46 additions & 0 deletions src/components/Favorite/Favorite.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useState } from "react";
import "./Favorite.css";

type FavoriteProps = {
pokemonId: number;
};

export function Favorite({ pokemonId }: FavoriteProps) {
// 1. Lazy initialization from localStorage
const [favorites, setFavorites] = useState<number[]>(() => {
try {
const storedFavorites = window.localStorage.getItem("favorites");
return storedFavorites ? JSON.parse(storedFavorites) : [];
} catch (error) {
console.error("Error reading localStorage", error);
return [];
}
});

const isFavorite = favorites.includes(pokemonId);

// 2. The click handler syncing State and Storage
const toggleFavorite = () => {
let updatedFavorites: number[];

if (isFavorite) {
updatedFavorites = favorites.filter((id) => id !== pokemonId);
} else {
updatedFavorites = [...favorites, pokemonId];
}

setFavorites(updatedFavorites);
window.localStorage.setItem("favorites", JSON.stringify(updatedFavorites));
};

// 3. Accessible UI rendering
return (
<button
onClick={toggleFavorite}
aria-label={isFavorite ? "Remove from favorites" : "Add to favorites"}
className="favorite-button"
>
{isFavorite ? "❤️" : "🤍"}
</button>
);
}