Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ sudo rsync -av --delete dist/ /var/www/html/project1/
echo "Deploy ferdig!"
```


## Prettier use

```
npm install
npm run format
```
npm run format
```
55 changes: 55 additions & 0 deletions src/Api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PokemonDetails> {
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,
};
}
156 changes: 156 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -363,3 +374,148 @@
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;
}
}
57 changes: 32 additions & 25 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
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 { listAllPokemon } from './Api';
import PokemonDetails from './PokemonDetails';

import './App.css';

const SEARCH_STORAGE_KEY = 'pokemon-search';
Expand All @@ -26,8 +28,9 @@ 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<number | null>(1);

// TanStack Query håndterer henting og caching av Pokémon-lista.
const {
Expand All @@ -41,7 +44,7 @@ function App() {
});

const [submittedQuery, setSubmittedQuery] = useState(
() => sessionStorage.getItem(SEARCH_STORAGE_KEY) ?? '',
() => sessionStorage.getItem(SEARCH_STORAGE_KEY) ?? ''
);

const [error, setError] = useState('');
Expand All @@ -52,24 +55,22 @@ 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)
: [];

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)
);
}

Expand Down Expand Up @@ -109,6 +110,14 @@ function App() {
Søk på navn, eller en del av et navn – for eksempel «char» eller
«pikachu».
</p>
</section>

<PokemonDetails
selectedPokemonId={selectedPokemonId}
setSelectedPokemonId={setSelectedPokemonId}
/>

<section>
<form className="search-form" onSubmit={handleSearch}>
<div className="search-input-wrapper">
<input
Expand Down Expand Up @@ -149,27 +158,21 @@ function App() {
</p>
)}

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

{!isError &&
!error &&
{!isError &&
!error &&
!isPending &&
submittedQuery &&
submittedQuery &&
pokemon.length === 0 && (
<p className="message error">
Fant ingen Pokémon som matcher «{submittedQuery}».
</p>
)}

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

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

{pokemon.map((entry) => (
<div className="song" key={entry.id}>
Expand All @@ -186,7 +189,11 @@ function App() {
>
★
</button>
<a className="song-link" href={entry.apiUrl} rel="noreferrer">
<button
className="song-link search-result"
onClick={() => setSelectedPokemonId(entry.id)}
type="button"
>
<img
src={entry.imageUrl}
alt=""
Expand All @@ -198,7 +205,7 @@ function App() {
<strong>{capitalize(entry.name)}</strong>
<small>#{entry.id} · se rå API-data</small>
</span>
</a>
</button>
</div>
))}
</section>
Expand Down
Loading