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
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,
};
}
168 changes: 168 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,160 @@
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 {
width: 100%;
margin-top: 16px;
border-spacing: 6px 0;
table-layout: fixed;
}

.pokemon-info th,
.pokemon-info td {
padding: 10px 12px;
text-align: left;
}

.pokemon-info th {
color: var(--text-h);
font-size: 14px;
font-weight: 700;
}

.pokemon-info td {
padding: 10px 12px;
border-radius: 10px;
background: var(--bg);
}

.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 {
width: 100%;
border-spacing: 6px;
table-layout: fixed;
}

.stat {
padding: 7px 9px;
border-radius: 8px;
background: var(--bg);
font-size: 13px;
}

.stat span,
.stat strong {
display: inline-block;
}

.stat strong {
float: right;
}

.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 {
border-spacing: 4px 0;
}

.stats-grid {
border-spacing: 4px;
}
}
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