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
27 changes: 27 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"format:check": "prettier --check ."
},
"dependencies": {
"@tanstack/react-query": "^5.103.0",
"pokenode-ts": "^2.3.1",
"react": "^19.2.8",
"react-dom": "^19.2.8"
Expand Down
34 changes: 5 additions & 29 deletions src/Api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,21 @@ type PokemonListResponse = {
results: Array<{ name: string; url: string }>;
};

// limit=100000 gir oss hele lista i ett kall. PokéAPI har ikke noe
// eget søke-endepunkt, så vi henter alle navnene én gang og filtrerer selv.
// PokéAPI har ikke eget søke-endepunkt, så vi henter Pokémon-lista
// og bruker den til lokal filtrering og søk
const LIST_ENDPOINT = 'https://pokeapi.co/api/v2/pokemon?limit=10000';

// Vi mellomlagrer lista i minnet så vi ikke henter den på nytt for hvert søk.
let cachedList: Array<{ name: string; url: string }> | null = null;

// Henter den komplette Pokémon-lista fra PokéAPI
// Caching av serverdata håndteres av TanStack Query i React-laget
async function fetchPokemonList() {
if (cachedList) {
return cachedList;
}

const response = await fetch(LIST_ENDPOINT);

if (!response.ok) {
throw new Error(`PokéAPI svarte med ${response.status}`);
}

const data = (await response.json()) as PokemonListResponse;
cachedList = data.results;
console.log(cachedList);
return cachedList;
return data.results;
}

// i Api.tsx
Expand All @@ -58,20 +51,3 @@ function idFromUrl(url: string): number {
const match = url.match(/\/pokemon\/(\d+)\//);
return match ? Number(match[1]) : 0;
}

export async function searchPokemon(query: string): Promise<Pokemon[]> {
const list = await fetchPokemonList();
const normalized = query.trim().toLowerCase();

return list
.filter((entry) => entry.name.includes(normalized))
.map((entry) => ({ entry, id: idFromUrl(entry.url) }))
.filter(({ id }) => id > 0)
.slice(0, 24)
.map(({ entry, id }) => ({
id,
name: entry.name,
imageUrl: `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${id}.png`,
apiUrl: `https://pokeapi.co/api/v2/pokemon/${id}`,
}));
}
61 changes: 39 additions & 22 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState } from 'react';
import type { FormEvent } from 'react';
import { searchPokemon } from './Api';
import { useQuery } from '@tanstack/react-query';
import { listAllPokemon } from './Api';
import type { Pokemon } from './Api';
import { useFavorites } from './useFavorites';
import FavoritesSidebar from './FavoritesSidebar';
Expand All @@ -11,12 +12,22 @@ function capitalize(text: string): string {
}
function App() {
const [query, setQuery] = useState('');

//TanStack Query håndterer henting og caching av Pokémon-lista.
const {
data: allPokemon = [],
isPending,
isError,
error: queryError,
} = useQuery({
queryKey: ['pokemon'],
queryFn: listAllPokemon,
});
const [pokemon, setPokemon] = useState<Pokemon[]>([]);
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const { favorites, isFavorite, toggleFavorite } = useFavorites();

async function handleSearch(event: FormEvent<HTMLFormElement>) {
function handleSearch(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const trimmedQuery = query.trim();

Expand All @@ -26,22 +37,18 @@ function App() {
return;
}

setIsLoading(true);
setError('');
const normalizedQuery = trimmedQuery.toLowerCase();

try {
const results = await searchPokemon(trimmedQuery);
setPokemon(results);
if (results.length === 0) {
setError(`Fant ingen Pokémon som matcher «${trimmedQuery}».`);
}
} catch (searchError) {
setPokemon([]);
setError(
searchError instanceof Error ? searchError.message : 'Noe gikk galt.'
);
} finally {
setIsLoading(false);
const results = allPokemon
.filter((entry) => entry.name.includes(normalizedQuery))
.slice(0, 24);

setPokemon(results);

if (results.length === 0) {
setError(`Fant ingen Pokémon som matcher «${trimmedQuery}».`);
} else {
setError('');
}
}

Expand All @@ -63,17 +70,27 @@ function App() {
placeholder="For eksempel: bulba"
aria-label="Søk etter Pokémon"
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Søker...' : 'Søk'}
<button type="submit" disabled={isPending}>
{isPending ? 'Laster Pokémon...' : 'Søk'}
</button>
</form>
</section>

<section className="results" aria-live="polite">
{error && <p className="message error">{error}</p>}
{!error && !isLoading && pokemon.length === 0 && (
{isError && (
<p className="message error">
Kunne ikke hente Pokémon: {queryError.message}
</p>
)}

{!isError && error && (
<p className="message error">{error}</p>
)}

{!isError && !error && !isPending && pokemon.length === 0 && (
<p className="message">Søkeresultatene dine vises her.</p>
)}

{pokemon.map((entry) => (
<div className="song" key={entry.id}>
<button
Expand Down
7 changes: 6 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import './index.css';
import App from './App.tsx';

const queryClient = new QueryClient();

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>
);