From 70229678bdf3b5e927481cf4cb68d6387323e929 Mon Sep 17 00:00:00 2001
From: William M Schmidt
Date: Thu, 17 Sep 2026 23:11:07 +0200
Subject: [PATCH 1/6] Added a card to display pokemon details
In the search page a card displays pokemon details.
Ran "npm run format" before commit.
- New function and datatype in Api.tsx to get pokemon details.
- The card was added directly into App.tsx. Should be its own component maybe.
- Updated the global App.css file with card css.
---
README.md | 6 +-
src/Api.tsx | 55 +++++++++++++++
src/App.css | 149 +++++++++++++++++++++++++++++++++++++++++
src/App.tsx | 124 ++++++++++++++++++++++++++++------
src/PokemonDetails.tsx | 48 +++++++++++++
src/main.tsx | 2 +-
6 files changed, 358 insertions(+), 26 deletions(-)
create mode 100644 src/PokemonDetails.tsx
diff --git a/README.md b/README.md
index 3c4c4ff..7112c3c 100644
--- a/README.md
+++ b/README.md
@@ -30,9 +30,9 @@ sudo rsync -av --delete dist/ /var/www/html/project1/
echo "Deploy ferdig!"
```
-
## Prettier use
+
```
npm install
-npm run format
-```
\ No newline at end of file
+npm run format
+```
diff --git a/src/Api.tsx b/src/Api.tsx
index fe0b53f..3088be9 100644
--- a/src/Api.tsx
+++ b/src/Api.tsx
@@ -51,3 +51,58 @@ function idFromUrl(url: string): number {
const match = url.match(/\/pokemon\/(\d+)\//);
return match ? Number(match[1]) : 0;
}
+
+/*
+ Type for displayed Pokemon details.
+*/
+export type PokemonDetails = {
+ id: number;
+ name: string;
+ generation: string;
+ types: string[];
+ stats: {
+ name: string;
+ value: number;
+ }[];
+ abilities: string[];
+ imageUrl: string;
+};
+
+/*
+ This function returns the relevant details for a specific Pokemon.
+ Must return a Promise that resolves as a PokemonDetails object.
+*/
+export async function getPokemonDetails(id: number): Promise {
+ const pokemonResponse = await fetch(pokemonApiUrl(id));
+
+ if (!pokemonResponse.ok) {
+ throw new Error(`PokéAPI svarte med ${pokemonResponse.status}`);
+ }
+
+ const data = await pokemonResponse.json();
+
+ const speciesResponse = await fetch(data.species.url);
+
+ if (!speciesResponse.ok) {
+ throw new Error(`Kunne ikke hente species-data`);
+ }
+
+ const speciesData = await speciesResponse.json();
+
+ return {
+ id: data.id,
+ name: data.name,
+ generation: speciesData.generation.name,
+ types: data.types.map((type: { type: { name: string } }) => type.type.name),
+ stats: data.stats.map(
+ (stat: { stat: { name: string }; base_stat: number }) => ({
+ name: stat.stat.name,
+ value: stat.base_stat,
+ })
+ ),
+ abilities: data.abilities.map(
+ (ability: { ability: { name: string } }) => ability.ability.name
+ ),
+ imageUrl: data.sprites.other['official-artwork'].front_default,
+ };
+}
diff --git a/src/App.css b/src/App.css
index d27aa90..7a0fa57 100644
--- a/src/App.css
+++ b/src/App.css
@@ -1,3 +1,14 @@
+/*
+ Used for search results in the pokedex search page.
+*/
+.search-result {
+ background: none;
+ border: none;
+ padding: 0;
+ font: inherit;
+ cursor: pointer;
+}
+
.search-page {
display: flex;
flex-direction: column;
@@ -319,3 +330,141 @@
padding-bottom: 4px;
}
}
+
+/*
+ CSS for pokemon detail card.
+*/
+.pokemon-card {
+ position: relative;
+ width: min(100%, 620px);
+ margin: 24px auto;
+ padding: 20px;
+ box-sizing: border-box;
+
+ border: 1px solid var(--accent-border);
+ border-radius: 18px;
+ background: var(--bg-soft);
+ text-align: left;
+ box-shadow: var(--shadow);
+}
+
+.pokemon-card-close {
+ position: absolute;
+ top: 10px;
+ right: 12px;
+
+ border: 0;
+ background: transparent;
+ color: var(--text);
+ font-size: 22px;
+ cursor: pointer;
+}
+
+.pokemon-card-header {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding-right: 24px;
+}
+
+.pokemon-card-header img {
+ width: 100px;
+ height: 100px;
+ object-fit: contain;
+}
+
+.pokemon-card h2 {
+ margin: 0;
+ font-size: 24px;
+}
+
+.pokemon-card h2 span {
+ color: var(--text);
+ font-size: 16px;
+ font-weight: 400;
+}
+
+.pokemon-generation {
+ margin-top: 4px;
+ color: var(--text);
+ font-size: 14px;
+}
+
+.pokemon-info {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+ margin-top: 16px;
+}
+
+.pokemon-info div {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 10px 12px;
+ border-radius: 10px;
+ background: var(--bg);
+}
+
+.pokemon-info strong,
+.pokemon-stats h3 {
+ color: var(--text-h);
+ font-size: 14px;
+}
+
+.pokemon-info span {
+ color: var(--text);
+ font-size: 14px;
+}
+
+.pokemon-stats {
+ margin-top: 16px;
+}
+
+.pokemon-stats h3 {
+ margin: 0 0 8px;
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 6px;
+}
+
+.stat {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 7px 9px;
+ border-radius: 8px;
+ background: var(--bg);
+ font-size: 13px;
+}
+
+.stat span {
+ color: var(--text);
+}
+
+.stat strong {
+ color: var(--text-h);
+}
+
+@media (max-width: 600px) {
+ .pokemon-card {
+ padding: 16px;
+ }
+
+ .pokemon-card-header img {
+ width: 80px;
+ height: 80px;
+ }
+
+ .pokemon-card h2 {
+ font-size: 20px;
+ }
+
+ .pokemon-info,
+ .stats-grid {
+ grid-template-columns: 1fr 1fr;
+ }
+}
diff --git a/src/App.tsx b/src/App.tsx
index 7bd108f..9885ee3 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,9 +1,10 @@
import { useState } from 'react';
import type { FormEvent } from 'react';
import { useQuery } from '@tanstack/react-query';
-import { listAllPokemon } from './Api';
import { useFavorites } from './useFavorites';
import FavoritesSidebar from './FavoritesSidebar';
+import { getPokemonDetails, listAllPokemon } from './Api';
+
import './App.css';
const SEARCH_STORAGE_KEY = 'pokemon-search';
@@ -14,7 +15,10 @@ function capitalize(text: string): string {
function App() {
const [query, setQuery] = useState(
- () => sessionStorage.getItem(SEARCH_STORAGE_KEY) ?? '',
+ () => sessionStorage.getItem(SEARCH_STORAGE_KEY) ?? ''
+ );
+ const [selectedPokemonId, setSelectedPokemonId] = useState(
+ null
);
// TanStack Query håndterer henting og caching av Pokémon-lista.
@@ -29,7 +33,7 @@ function App() {
});
const [submittedQuery, setSubmittedQuery] = useState(
- () => sessionStorage.getItem(SEARCH_STORAGE_KEY) ?? '',
+ () => sessionStorage.getItem(SEARCH_STORAGE_KEY) ?? ''
);
const [error, setError] = useState('');
@@ -38,12 +42,20 @@ function App() {
const pokemon = submittedQuery
? allPokemon
- .filter((entry) =>
- entry.name.includes(submittedQuery.toLowerCase()),
- )
- .slice(0, 24)
+ .filter((entry) => entry.name.includes(submittedQuery.toLowerCase()))
+ .slice(0, 24)
: [];
+ const {
+ data: selectedPokemon,
+ isPending: isDetailsPending,
+ isError: isDetailsError,
+ } = useQuery({
+ queryKey: ['pokemon-details', selectedPokemonId],
+ queryFn: () => getPokemonDetails(selectedPokemonId!),
+ enabled: selectedPokemonId !== null,
+ });
+
function handleSearch(event: FormEvent) {
event.preventDefault();
const trimmedQuery = query.trim();
@@ -69,6 +81,76 @@ function App() {
Søk på navn, eller en del av et navn – for eksempel «char» eller
«pikachu».
+
+
+
+
+
+ {isDetailsPending && Laster detaljer...
}
+
+ {isDetailsError && (
+ Kunne ikke hente detaljer.
+ )}
+
+ {selectedPokemon && (
+ <>
+
+

+
+
+
+ {capitalize(selectedPokemon.name)}{' '}
+ #{selectedPokemon.id}
+
+
+ {capitalize(selectedPokemon.generation.replace('-', ' '))}
+
+
+
+
+
+
+ Type
+
+ {selectedPokemon.types.map(capitalize).join(', ')}
+
+
+
+
+ Abilities
+
+ {selectedPokemon.abilities.map(capitalize).join(', ')}
+
+
+
+
+
+
Base stats
+
+
+ {selectedPokemon.stats.map((stat) => (
+
+ {capitalize(stat.name)}
+ {stat.value}
+
+ ))}
+
+
+ >
+ )}
+
+
+
diff --git a/src/PokemonDetails.tsx b/src/PokemonDetails.tsx
new file mode 100644
index 0000000..c063ac8
--- /dev/null
+++ b/src/PokemonDetails.tsx
@@ -0,0 +1,48 @@
+import { useQuery } from '@tanstack/react-query';
+import type { Pokemon } from './Api';
+import { getPokemonDetails } from './Api';
+import './App.css';
+
+const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
+
+function PokemonDetails({ pokemon }: { pokemon: Pokemon }) {
+ const { data, isPending, isError } = useQuery({
+ queryKey: ['pokemon-details', pokemon.id],
+ queryFn: () => getPokemonDetails(pokemon.id),
+ });
+
+ if (isPending) {
+ return Loading Pokémon details...
;
+ }
+
+ if (isError || !data) {
+ return Could not load Pokémon details.
;
+ }
+
+ return (
+
+
+ {capitalize(data.name)} (#{data.id})
+
+
+
+
+ Types
+ {data.types.map(capitalize).join(', ')}
+
+ Abilities
+ {data.abilities.map(capitalize).join(', ')}
+
+ Stats
+
+ {data.stats.map((stat) => (
+ -
+ {capitalize(stat.name)}: {stat.value}
+
+ ))}
+
+
+ );
+}
+
+export default PokemonDetails;
diff --git a/src/main.tsx b/src/main.tsx
index eb0dde4..32f177e 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,6 +1,6 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import './index.css';
import App from './App.tsx';
From 018322b8b567ef1caccde3af0d93db79e0b50ad2 Mon Sep 17 00:00:00 2001
From: William M Schmidt
Date: Thu, 17 Sep 2026 23:15:25 +0200
Subject: [PATCH 2/6] Changed default pokemon to #1 so it always loads
something.
---
src/App.tsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/App.tsx b/src/App.tsx
index 9885ee3..8cf0f6a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -17,9 +17,7 @@ function App() {
const [query, setQuery] = useState(
() => sessionStorage.getItem(SEARCH_STORAGE_KEY) ?? ''
);
- const [selectedPokemonId, setSelectedPokemonId] = useState(
- null
- );
+ const [selectedPokemonId, setSelectedPokemonId] = useState(1);
// TanStack Query håndterer henting og caching av Pokémon-lista.
const {
From 89a4e945ef4d69b50571cf39e873112a923533e7 Mon Sep 17 00:00:00 2001
From: William M Schmidt
Date: Fri, 18 Sep 2026 13:02:58 +0200
Subject: [PATCH 3/6] App.tsx renders component from PokemonDetails.tsx
No more UI code directly in App.tsx. Simply a call to the component. Passing state props.
---
src/App.css | 7 +++
src/App.tsx | 92 ++++-------------------------------
src/PokemonDetails.tsx | 106 ++++++++++++++++++++++++++++++-----------
3 files changed, 96 insertions(+), 109 deletions(-)
diff --git a/src/App.css b/src/App.css
index 31cb7b8..367b0ff 100644
--- a/src/App.css
+++ b/src/App.css
@@ -375,6 +375,13 @@
}
}
+
+
+
+
+
+
+
/*
CSS for pokemon detail card.
*/
diff --git a/src/App.tsx b/src/App.tsx
index 247661e..d0f592f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -3,7 +3,8 @@ import type { FormEvent } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useFavorites } from './useFavorites';
import FavoritesSidebar from './FavoritesSidebar';
-import { getPokemonDetails, listAllPokemon } from './Api';
+import { listAllPokemon } from './Api';
+import PokemonDetails from './PokemonDetails';
import './App.css';
@@ -58,37 +59,24 @@ function App() {
.slice(0, 24)
: [];
- const {
- data: selectedPokemon,
- isPending: isDetailsPending,
- isError: isDetailsError,
- } = useQuery({
- queryKey: ['pokemon-details', selectedPokemonId],
- queryFn: () => getPokemonDetails(selectedPokemonId!),
- enabled: selectedPokemonId !== null,
- });
-
-
-
function addToRecentSearches(trimmedQuery: string) {
const withoutDuplicate = recentSearches.filter(
- (entry) => entry.toLowerCase() !== trimmedQuery.toLowerCase(),
+ (entry) => entry.toLowerCase() !== trimmedQuery.toLowerCase()
);
const updated = [trimmedQuery, ...withoutDuplicate].slice(
0,
- MAX_RECENT_SEARCHES,
+ MAX_RECENT_SEARCHES
);
setRecentSearches(updated);
sessionStorage.setItem(
RECENT_SEARCHES_STORAGE_KEY,
- JSON.stringify(updated),
+ JSON.stringify(updated)
);
}
function runSearch(rawQuery: string) {
const trimmedQuery = rawQuery.trim();
-
if (!trimmedQuery) {
setSubmittedQuery('');
setError('Skriv inn et navn (eller en del av et navn) først.');
@@ -124,71 +112,11 @@ function App() {
-
-
-
- {isDetailsPending && Laster detaljer...
}
-
- {isDetailsError && (
- Kunne ikke hente detaljer.
- )}
-
- {selectedPokemon && (
- <>
-
-

-
-
-
- {capitalize(selectedPokemon.name)}{' '}
- #{selectedPokemon.id}
-
-
- {capitalize(selectedPokemon.generation.replace('-', ' '))}
-
-
-
-
-
-
- Type
-
- {selectedPokemon.types.map(capitalize).join(', ')}
-
-
-
-
- Abilities
-
- {selectedPokemon.abilities.map(capitalize).join(', ')}
-
-
-
-
-
-
Base stats
-
-
- {selectedPokemon.stats.map((stat) => (
-
- {capitalize(stat.name)}
- {stat.value}
-
- ))}
-
-
- >
- )}
+
diff --git a/src/PokemonDetails.tsx b/src/PokemonDetails.tsx
index c063ac8..c59912b 100644
--- a/src/PokemonDetails.tsx
+++ b/src/PokemonDetails.tsx
@@ -1,46 +1,98 @@
import { useQuery } from '@tanstack/react-query';
-import type { Pokemon } from './Api';
import { getPokemonDetails } from './Api';
+import type { Dispatch, SetStateAction } from 'react';
import './App.css';
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
-function PokemonDetails({ pokemon }: { pokemon: Pokemon }) {
- const { data, isPending, isError } = useQuery({
- queryKey: ['pokemon-details', pokemon.id],
- queryFn: () => getPokemonDetails(pokemon.id),
+// The PokemonDetails type that is used to fetch state from App.tsx to be used and manipulated in the details card.
+type PokemonDetailsProps = {
+ selectedPokemonId: number | null;
+ setSelectedPokemonId: Dispatch>;
+};
+
+function PokemonDetails({
+ selectedPokemonId,
+ setSelectedPokemonId,
+}: PokemonDetailsProps) {
+
+ // TanStack Query is used to fetch the details of the selected Pokémon based on its ID.
+ const {
+ data: selectedPokemon,
+ isPending,
+ isError,
+ } = useQuery({
+ queryKey: ['pokemon-details', selectedPokemonId],
+ queryFn: () => getPokemonDetails(selectedPokemonId!),
+ enabled: selectedPokemonId !== null, // Query only runs if the selectedPokemonId is not null.
});
if (isPending) {
return Loading Pokémon details...
;
}
- if (isError || !data) {
+ if (isError || !selectedPokemon) {
return Could not load Pokémon details.
;
}
return (
-
-
- {capitalize(data.name)} (#{data.id})
-
-
-
-
- Types
- {data.types.map(capitalize).join(', ')}
-
- Abilities
- {data.abilities.map(capitalize).join(', ')}
-
- Stats
-
- {data.stats.map((stat) => (
- -
- {capitalize(stat.name)}: {stat.value}
-
- ))}
-
+
+
+
+ {selectedPokemon && (
+ <>
+
+

+
+
+
+ {capitalize(selectedPokemon.name)}{' '}
+ #{selectedPokemon.id}
+
+
+ {capitalize(selectedPokemon.generation.replace('-', ' '))}
+
+
+
+
+
+
+ Type
+ {selectedPokemon.types.map(capitalize).join(', ')}
+
+
+
+ Abilities
+
+ {selectedPokemon.abilities.map(capitalize).join(', ')}
+
+
+
+
+
+
Base stats
+
+
+ {selectedPokemon.stats.map((stat) => (
+
+ {capitalize(stat.name)}
+ {stat.value}
+
+ ))}
+
+
+ >
+ )}
);
}
From fd129c1ff2462e44f060a2b97ec4a81d9c41e92f Mon Sep 17 00:00:00 2001
From: William M Schmidt
Date: Fri, 18 Sep 2026 13:05:49 +0200
Subject: [PATCH 4/6] Removed unnecessary section wrapper
---
src/App.tsx | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/App.tsx b/src/App.tsx
index d0f592f..0a3ba26 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -112,12 +112,10 @@ function App() {
-
-
+