From 086c2c041facab006d35253a5181114aae72fd9e Mon Sep 17 00:00:00 2001 From: Oscar Bakkeng Date: Wed, 16 Sep 2026 14:06:35 +0200 Subject: [PATCH] Installed and run Prettier on the project. Do npm insatll to install --- .prettierignore | 2 + .prettierrc | 7 ++++ CHANGES.md | 10 +++-- README.md | 25 +++++++----- VEIEN-VIDERE.md | 84 ++++++++++++++++++++-------------------- eslint.config.js | 14 +++---- package-lock.json | 32 +++++++++++++++ package.json | 6 ++- src/Api.tsx | 79 ++++++++++++++++++------------------- src/App.css | 17 ++++++-- src/App.tsx | 65 +++++++++++++++++-------------- src/FavoritesSidebar.tsx | 22 ++++++----- src/main.tsx | 12 +++--- src/useFavorites.ts | 36 +++++++++-------- vite.config.ts | 13 +++---- 15 files changed, 246 insertions(+), 178 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b0ec5b3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 80 +} diff --git a/CHANGES.md b/CHANGES.md index 03fcead..64341e8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ visuell make-over av søkesiden, en ny favoritt-funksjon lagret i **Hvor:** `src/index.css`, `src/App.css`, `index.html` **Hva:** + - Byttet fargepalett fra generisk lilla til et Pokéball-inspirert tema: rødt (`--accent`) og gult (`--gold`) som aksentfarger, definert som CSS-variabler i `:root` (både lys og mørk modus er oppdatert). @@ -29,7 +30,7 @@ farger og form som minner om selve merkevaren. **Hvor:** `src/App.tsx` -**Hva:** Fjernet `
`-blokken som viste *alle* +**Hva:** Fjernet `
`-blokken som viste _alle_ Pokémon i en ustylet `
    ` (rød tekst, sirkel-bullets), samt `allPokemon`-state og `useEffect`-kallet som hentet hele listen (`listAllPokemon()`) ved oppstart. @@ -44,6 +45,7 @@ gjennom alle»-visning). ## 3. Favoritter (ny funksjon, lagret i `localStorage`) **Hvor:** + - `src/useFavorites.ts` (ny fil) – egen hook som eier all lagringslogikk. - `src/FavoritesSidebar.tsx` (ny fil) – komponenten som viser favorittene. - `src/App.tsx` – bruker hooken, viser en ★-knapp på hvert søkeresultat, @@ -52,6 +54,7 @@ gjennom alle»-visning). mobiltilpasning. **Hva:** + - Brukeren kan trykke på en ★-knapp på et hvilket som helst søkeresultat for å legge det til eller fjerne det fra favorittene sine. - Favorittene lagres i `localStorage` under nøkkelen `pokedex-favorites` @@ -65,7 +68,7 @@ gjennom alle»-visning). (`@media max-width: 900px`) gjør den om til en fast bunn-bar med horisontal scroll, slik at favorittene fortsatt er lett tilgjengelige uten å ta opp all skjermplassen på en liten skjerm. Det er altså - *ingen* egen mobil-komponent – responsiviteten løses kun med CSS. + _ingen_ egen mobil-komponent – responsiviteten løses kun med CSS. - Hver favoritt kan fjernes direkte fra sidebaren med en ×-knapp. **Hvorfor `localStorage`:** Favoritter er noe brukeren typisk vil ha @@ -86,12 +89,13 @@ Vi har **ikke** implementert dette – bare et forslag til neste steg: `sessionStorage` (f.eks. under nøkkelen `pokedex-last-search`). **Hvorfor dette passer godt til `sessionStorage` og ikke `localStorage`:** + - `sessionStorage` overlever en side-oppdatering (F5), men blir tømt når fanen lukkes. Det er akkurat riktig levetid for et søk: nyttig å beholde om brukeren ved et uhell laster siden på nytt midt i en søkeøkt, men det gir ikke mening at et gammelt søk skal ligge og vente neste gang brukeren åpner siden i en ny fane uker senere (i motsetning - til favoritter, som *skal* vare). + til favoritter, som _skal_ vare). - Det er en pedagogisk fin kontrast til favoritt-funksjonen: samme mønster (les/skriv til Web Storage), men bevisst forskjellig levetid fordi dataene har forskjellig «naturlig» levetid. diff --git a/README.md b/README.md index 5566453..3c4c4ff 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,25 @@ # Pokedex ## Koble til VM (hosting) -Hvordan koble til VM: - -``` ssh @it2810-18.idi.ntnu.no ``` +Hvordan koble til VM: +`ssh @it2810-18.idi.ntnu.no` ## Nettside: -http://it2810-18.idi.ntnu.no/project1/index.html - -Filene ligger i vm under ```/var/www/html/project1``` +http://it2810-18.idi.ntnu.no/project1/index.html +Filene ligger i vm under `/var/www/html/project1` ## Har du gjort endringer du vil ha på nettsiden live? -```npm run build``` inni prosjekt mappen, deretter flytt filene som er inni ```dist``` mappen til ```/var/www/html/project1``` - -Da vil de vises på nettsiden. +`npm run build` inni prosjekt mappen, deretter flytt filene som er inni `dist` mappen til `/var/www/html/project1` +Da vil de vises på nettsiden. ## Script for å deploye endringer til nettside hosting (OBS OBS, dette er ikke til git, men bare fra VM directory til nettside hosting directory) prosjekt -> /var/www/html/ + ``` #!/bin/bash cd ~/T18-Project-1 @@ -29,5 +27,12 @@ git pull npm install npm run build sudo rsync -av --delete dist/ /var/www/html/project1/ -echo "Deploy ferdig!" +echo "Deploy ferdig!" +``` + + +## Prettier use +``` +npm install +npm run format ``` \ No newline at end of file diff --git a/VEIEN-VIDERE.md b/VEIEN-VIDERE.md index 55b7612..1f89f6a 100644 --- a/VEIEN-VIDERE.md +++ b/VEIEN-VIDERE.md @@ -14,7 +14,7 @@ fortsatt flere ting som er **eksplisitte, harde krav** i prosjektbeskrivelsen — ikke bare finpuss. De viktigste hullene er: 1. Ingen bruk av **TanStack Query** (hardt teknisk krav). -2. **Ingen bruk av `sessionStorage`** i det hele tatt (krav er *begge* +2. **Ingen bruk av `sessionStorage`** i det hele tatt (krav er _begge_ Web Storage-mekanismene, ikke bare `localStorage`). 3. Ingen **filter/sorterings**-funksjon (dette er et eget funksjonelt krav, atskilt fra fritekstsøket som allerede finnes). @@ -32,7 +32,7 @@ prosjektbeskrivelsen — ikke bare finpuss. De viktigste hullene er: Ingen grunn til bekymring i seg selv — dette er en tidlig fase av prosjektet — men punktene over bør løses før første innlevering, siden -vurderingskriteriene sier løsningen skal svare på *alle* funksjonelle og +vurderingskriteriene sier løsningen skal svare på _alle_ funksjonelle og tekniske krav, og at det ikke skal være unødvendige API-kall eller lint/test-advarsler. @@ -42,55 +42,55 @@ Status: ✅ på plass · 🟡 delvis / må endres · ❌ mangler helt ### Funksjonelle krav -| Krav | Status | Kommentar | -|---|---|---| -| Presenter én ressurs om gangen, med blafunksjon frem/tilbake og hopp til spesifikk ressurs via en liste | ❌ | I dag vises et *grid* av alle søketreff samtidig. Må bygges om til en enkelt-visning med «Forrige/Neste» og en velger (f.eks. nedtrekksliste eller søk-og-velg) for å hoppe direkte til en Pokémon. | -| Valg (filtrering/sortering) som endrer utvalg/presentasjon, og som huskes ved reload | ❌ | Finnes ikke i dag. Fritekstsøket er ikke det samme som filtrering/sortering. Naturlig kandidat: sorter etter navn/nummer, filtrer på type. Bør lagres i `sessionStorage` (se under). | -| Favoritter (stjerne/hjerte), huskes selv om nettleseren lukkes og åpnes igjen | ✅ | Implementert denne økten i `localStorage` (`pokedex-favorites`), se `CHANGES.md`. | -| Responsivt design (desktop + mobil, både portrett og landskap) | 🟡 | Sidebar/bunn-bar-løsningen for favoritter er testet i portrett-mobil og desktop. Bør også sjekkes i **landskap på mobil** (kort skjermhøyde) siden bunn-baren tar opptil 140px i høyden. | -| Estetisk og ryddig utforming | ✅ | Nytt Pokéball-tema, ryddigere kort, konsistent spacing (denne økten). Subjektivt krav, men det er lagt ned reelt arbeid. | +| Krav | Status | Kommentar | +| ------------------------------------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Presenter én ressurs om gangen, med blafunksjon frem/tilbake og hopp til spesifikk ressurs via en liste | ❌ | I dag vises et _grid_ av alle søketreff samtidig. Må bygges om til en enkelt-visning med «Forrige/Neste» og en velger (f.eks. nedtrekksliste eller søk-og-velg) for å hoppe direkte til en Pokémon. | +| Valg (filtrering/sortering) som endrer utvalg/presentasjon, og som huskes ved reload | ❌ | Finnes ikke i dag. Fritekstsøket er ikke det samme som filtrering/sortering. Naturlig kandidat: sorter etter navn/nummer, filtrer på type. Bør lagres i `sessionStorage` (se under). | +| Favoritter (stjerne/hjerte), huskes selv om nettleseren lukkes og åpnes igjen | ✅ | Implementert denne økten i `localStorage` (`pokedex-favorites`), se `CHANGES.md`. | +| Responsivt design (desktop + mobil, både portrett og landskap) | 🟡 | Sidebar/bunn-bar-løsningen for favoritter er testet i portrett-mobil og desktop. Bør også sjekkes i **landskap på mobil** (kort skjermhøyde) siden bunn-baren tar opptil 140px i høyden. | +| Estetisk og ryddig utforming | ✅ | Nytt Pokéball-tema, ryddigere kort, konsistent spacing (denne økten). Subjektivt krav, men det er lagt ned reelt arbeid. | ### Tekniske krav -| Krav | Status | Kommentar | -|---|---|---| -| God bruk av HTML mtp. tilgjengelighet | 🟡 | Semantisk HTML (`main`, `section`, `aside`, `form`, `button`) og `aria-label`/`aria-pressed` på favoritt-knappen er på plass. Bør gå én runde til med fargekontrast (rødt på hvitt/rødt på grått) og vurdere om bilder trenger bedre `alt`-tekst enn `alt=""` (i dag er navnet alltid synlig som tekst ved siden av, så det er trolig OK, men verdt en bevisst vurdering). | -| TypeScript + React | ✅ | På plass. | -| Bruk av React state og props | ✅ | På plass (`useState`, egne hooks, props mellom `App` og `FavoritesSidebar`). | -| Henter data fra REST API **og bruker TanStack Query** | ❌ | I dag brukes vanlig `fetch` direkte i `Api.tsx`. TanStack Query er ikke installert. Dette er et eksplisitt, navngitt krav — må på plass. | -| Web Storage API — **både** `localStorage` og `sessionStorage` (evt. IndexedDB) | 🟡 | Kun `localStorage` er tatt i bruk (favoritter). `sessionStorage` er foreløpig bare *foreslått* i `CHANGES.md`, ikke implementert. Filter/sorteringsvalget over er en naturlig kandidat for `sessionStorage`. | -| Responsivt design | ✅ | Se over. | -| Ingen tredjeparts UI-komponenter, kun plain CSS | ✅ | Ingen UI-bibliotek i bruk. Nunito-fonten som ble lagt til denne økten er en webfont, ikke en UI-komponent eller et CSS-rammeverk, så den bryter ikke kravet — men er verdt å nevne i dokumentasjonen som et bevisst valg. | +| Krav | Status | Kommentar | +| ------------------------------------------------------------------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| God bruk av HTML mtp. tilgjengelighet | 🟡 | Semantisk HTML (`main`, `section`, `aside`, `form`, `button`) og `aria-label`/`aria-pressed` på favoritt-knappen er på plass. Bør gå én runde til med fargekontrast (rødt på hvitt/rødt på grått) og vurdere om bilder trenger bedre `alt`-tekst enn `alt=""` (i dag er navnet alltid synlig som tekst ved siden av, så det er trolig OK, men verdt en bevisst vurdering). | +| TypeScript + React | ✅ | På plass. | +| Bruk av React state og props | ✅ | På plass (`useState`, egne hooks, props mellom `App` og `FavoritesSidebar`). | +| Henter data fra REST API **og bruker TanStack Query** | ❌ | I dag brukes vanlig `fetch` direkte i `Api.tsx`. TanStack Query er ikke installert. Dette er et eksplisitt, navngitt krav — må på plass. | +| Web Storage API — **både** `localStorage` og `sessionStorage` (evt. IndexedDB) | 🟡 | Kun `localStorage` er tatt i bruk (favoritter). `sessionStorage` er foreløpig bare _foreslått_ i `CHANGES.md`, ikke implementert. Filter/sorteringsvalget over er en naturlig kandidat for `sessionStorage`. | +| Responsivt design | ✅ | Se over. | +| Ingen tredjeparts UI-komponenter, kun plain CSS | ✅ | Ingen UI-bibliotek i bruk. Nunito-fonten som ble lagt til denne økten er en webfont, ikke en UI-komponent eller et CSS-rammeverk, så den bryter ikke kravet — men er verdt å nevne i dokumentasjonen som et bevisst valg. | ### Utvikling og testing -| Krav | Status | Kommentar | -|---|---|---| -| Node v24.6.X+ / npm v11+ | 🟡 | Ikke låst noe sted i prosjektet (f.eks. `engines` i `package.json`), så det er ikke sjekkbart/reproduserbart for andre. Bør legges til. | -| Vite + React + TypeScript-oppsett | ✅ | På plass. | -| Kildekode på **git.ntnu.no** | ✅ | `origin`-remote peker allerede til `git@git.ntnu.no:IT2810-H26/T18-Project-1.git`. `github`-remote ser ut til kun å være en privat speiling for GitHub Pages-demo på mobil — helt greit så lenge **innlevering** skjer via git.ntnu.no. | -| Branching + issues koblet til commits/PR-er | 🟡 | Kun to brancher så langt (`main`, `base-site`), ingen synlige issue-koblinger i commit-historikken enda. Naturlig siden prosjektet er tidlig i løpet, men bør etableres som vane fremover (se plan under). | -| Systematisk bruk av PR og kodegjennomgang | 🟡 | `CODEOWNERS` er satt opp (krever godkjenning fra gruppa), men historikken viser ingen PR-er enda — alt er committet rett på branch. | -| ESLint + Prettier | 🟡 | ESLint er satt opp og kjører rent (`npm run lint`). **Prettier er ikke installert.** | -| Vitest satt opp | ❌ | Ikke installert, ingen testfiler finnes. | -| Snapshot-test | ❌ | Mangler (avhenger av Vitest-oppsett). | -| Testing av egne komponenter (props, state, brukerinteraksjon) | ❌ | Mangler. | -| Mocking så tester ikke treffer ekte API | ❌ | Mangler (ingen tester enda i det hele tatt). | -| Testet på mainstream nettlesere/mobile enheter | 🟡 | Testet visuelt i Chrome (desktop + simulert mobilbredde) denne økten. Bør suppleres med faktisk test på minst én ekte mobilenhet og én annen nettleser (Firefox/Safari) før innlevering. | -| README dokumenterer valg, hva som er testet, og **KI-bruk** | ❌ | README inneholder i dag kun VM-tilkobling/deploy-instruksjoner. Må utvides betydelig. | +| Krav | Status | Kommentar | +| ------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Node v24.6.X+ / npm v11+ | 🟡 | Ikke låst noe sted i prosjektet (f.eks. `engines` i `package.json`), så det er ikke sjekkbart/reproduserbart for andre. Bør legges til. | +| Vite + React + TypeScript-oppsett | ✅ | På plass. | +| Kildekode på **git.ntnu.no** | ✅ | `origin`-remote peker allerede til `git@git.ntnu.no:IT2810-H26/T18-Project-1.git`. `github`-remote ser ut til kun å være en privat speiling for GitHub Pages-demo på mobil — helt greit så lenge **innlevering** skjer via git.ntnu.no. | +| Branching + issues koblet til commits/PR-er | 🟡 | Kun to brancher så langt (`main`, `base-site`), ingen synlige issue-koblinger i commit-historikken enda. Naturlig siden prosjektet er tidlig i løpet, men bør etableres som vane fremover (se plan under). | +| Systematisk bruk av PR og kodegjennomgang | 🟡 | `CODEOWNERS` er satt opp (krever godkjenning fra gruppa), men historikken viser ingen PR-er enda — alt er committet rett på branch. | +| ESLint + Prettier | 🟡 | ESLint er satt opp og kjører rent (`npm run lint`). **Prettier er ikke installert.** | +| Vitest satt opp | ❌ | Ikke installert, ingen testfiler finnes. | +| Snapshot-test | ❌ | Mangler (avhenger av Vitest-oppsett). | +| Testing av egne komponenter (props, state, brukerinteraksjon) | ❌ | Mangler. | +| Mocking så tester ikke treffer ekte API | ❌ | Mangler (ingen tester enda i det hele tatt). | +| Testet på mainstream nettlesere/mobile enheter | 🟡 | Testet visuelt i Chrome (desktop + simulert mobilbredde) denne økten. Bør suppleres med faktisk test på minst én ekte mobilenhet og én annen nettleser (Firefox/Safari) før innlevering. | +| README dokumenterer valg, hva som er testet, og **KI-bruk** | ❌ | README inneholder i dag kun VM-tilkobling/deploy-instruksjoner. Må utvides betydelig. | ### Levering -| Krav | Status | Kommentar | -|---|---|---| -| Repo på git.ntnu.no under gruppas GitHub(NTNU)-gruppe, navn «Prosjekt 1» | 🟡 | Repoet ligger på git.ntnu.no i riktig gruppe (`IT2810-H26`), men selve repo-navnet er `T18-Project-1`, ikke «Prosjekt 1». Verdt å avklare med faglærer/TA om det faktiske navnet på repoet må matche eksakt, eller om det holder at det ligger i riktig gruppe. | -| Appen kjører under Apache på gruppas VM | ✅ | Dokumentert og satt opp i README/`deploy.sh`. | -| Kort notat om individuell arbeidsinnsats på Blackboard | ⏳ | Utenfor kodebasen — husk å gjøre dette som egen leveranse. | +| Krav | Status | Kommentar | +| ------------------------------------------------------------------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Repo på git.ntnu.no under gruppas GitHub(NTNU)-gruppe, navn «Prosjekt 1» | 🟡 | Repoet ligger på git.ntnu.no i riktig gruppe (`IT2810-H26`), men selve repo-navnet er `T18-Project-1`, ikke «Prosjekt 1». Verdt å avklare med faglærer/TA om det faktiske navnet på repoet må matche eksakt, eller om det holder at det ligger i riktig gruppe. | +| Appen kjører under Apache på gruppas VM | ✅ | Dokumentert og satt opp i README/`deploy.sh`. | +| Kort notat om individuell arbeidsinnsats på Blackboard | ⏳ | Utenfor kodebasen — husk å gjøre dette som egen leveranse. | ## Den viktigste avveiingen: «ikke mellomlagre data» vs. dagens søk -Prosjektbeskrivelsen sier eksplisitt («NB!»): *hent data on the fly via -REST-APIet, ikke mellomlagre data.* Dagens `Api.tsx` henter derimot +Prosjektbeskrivelsen sier eksplisitt («NB!»): _hent data on the fly via +REST-APIet, ikke mellomlagre data._ Dagens `Api.tsx` henter derimot **hele** Pokémon-lista (alle ~10 000 rader) én gang og lagrer den i en modulvariabel (`cachedList`) som så filtreres lokalt for hvert søk. Dette er i praksis å bygge en egen lokal kopi av datasettet — nøyaktig det @@ -111,7 +111,7 @@ uten å laste ned hele datasettet på forhånd, er et par alternativer: - Pokémon har stort sett sammenhengende id-er (1–1010+), så en velger kan bygges på kjente id-er/navn uten noe eget API-kall i det hele tatt, og selve Pokémon-dataene hentes først når brukeren faktisk velger en. -- Alternativt: hent kun *navn + id* via PokéAPI sitt liste-endepunkt med +- Alternativt: hent kun _navn + id_ via PokéAPI sitt liste-endepunkt med paginering (`limit`/`offset`) for én side om gangen i stedet for alt på én gang, slik at det er en lett navigasjonsliste og ikke en full lokal kopi av ressursene. @@ -160,7 +160,7 @@ løse dem sammen. 11. Tilgjengelighetsrunde: fargekontrast, `alt`-tekster, og gjerne et par minutter med skjermleser (VoiceOver/NVDA) på søk + favoritter. -12. Faktisk test på minst én ekte mobilenhet (portrett *og* landskap) og +12. Faktisk test på minst én ekte mobilenhet (portrett _og_ landskap) og én ekstra nettleser utover Chrome. 13. Siste sjekk rett før innlevering: `npm run lint`, `npm run build` og testene kjører helt uten feil eller advarsler (eksplisitt @@ -171,7 +171,7 @@ løse dem sammen. - Er PokéAPI sitt liste-endepunkt (som må hentes i sin helhet for at søk/filtrering skal fungere uten et dedikert søke-API) i strid med «ikke mellomlagre»-kravet, eller er det akseptert som en nødvendig - navigasjonsliste så lenge selve *ressursdataene* alltid hentes on the + navigasjonsliste så lenge selve _ressursdataene_ alltid hentes on the fly? Løsningsforslaget over (id-basert velger + on-the-fly-henting per ressurs) er ment å unngå problemet helt, men verdt å bekrefte. - Må repoet hete eksakt «Prosjekt 1», eller holder det at det ligger i diff --git a/eslint.config.js b/eslint.config.js index ef614d2..973e273 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,9 +1,9 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import { defineConfig, globalIgnores } from 'eslint/config'; export default defineConfig([ globalIgnores(['dist']), @@ -19,4 +19,4 @@ export default defineConfig([ globals: globals.browser, }, }, -]) +]); diff --git a/package-lock.json b/package-lock.json index d8bb27f..478fac6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,9 +23,11 @@ "@vitejs/plugin-react": "^6.1.0", "babel-plugin-react-compiler": "^1.0.0", "eslint": "^10.9.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", "globals": "^17.11.0", + "prettier": "^3.9.7", "typescript": "~6.0.2", "typescript-eslint": "^8.67.0", "vite": "^8.2.2" @@ -1543,6 +1545,21 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", @@ -2449,6 +2466,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.7.tgz", + "integrity": "sha512-T3mF5P7HFzZVLDQuCUNtJj6WK1pcpLMweMNUVwGb01bLNkH9AkKxtZvmInw8K6bnn2O3d6/5RHl6zSHiBAD0Og==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index a387582..7e1c188 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "dependencies": { "pokenode-ts": "^2.3.1", @@ -25,9 +27,11 @@ "@vitejs/plugin-react": "^6.1.0", "babel-plugin-react-compiler": "^1.0.0", "eslint": "^10.9.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", "globals": "^17.11.0", + "prettier": "^3.9.7", "typescript": "~6.0.2", "typescript-eslint": "^8.67.0", "vite": "^8.2.2" diff --git a/src/Api.tsx b/src/Api.tsx index 98f43df..939d521 100644 --- a/src/Api.tsx +++ b/src/Api.tsx @@ -1,44 +1,42 @@ - - export type Pokemon = { - id: number - name: string - imageUrl: string - apiUrl: string -} + id: number; + name: string; + imageUrl: string; + apiUrl: string; +}; // Slik ser svaret fra liste-endepunktet ut (bare feltene vi bruker). type PokemonListResponse = { - results: Array<{ name: string; url: string }> -} + 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. -const LIST_ENDPOINT = 'https://pokeapi.co/api/v2/pokemon?limit=10000' +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 +let cachedList: Array<{ name: string; url: string }> | null = null; async function fetchPokemonList() { - if (cachedList) { - return cachedList - } + if (cachedList) { + return cachedList; + } - const response = await fetch(LIST_ENDPOINT) + const response = await fetch(LIST_ENDPOINT); - if (!response.ok) { - throw new Error(`PokéAPI svarte med ${response.status}`) - } + 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 + const data = (await response.json()) as PokemonListResponse; + cachedList = data.results; + console.log(cachedList); + return cachedList; } // i Api.tsx export async function listAllPokemon(): Promise { - const list = await fetchPokemonList() + const list = await fetchPokemonList(); return list .map((entry) => ({ entry, id: idFromUrl(entry.url) })) .filter(({ id }) => id > 0) @@ -47,34 +45,33 @@ export async function listAllPokemon(): Promise { 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}`, - })) + })); } - export function pokemonApiUrl(id: number) { - return `https://pokeapi.co/api/v2/pokemon/${id}` + return `https://pokeapi.co/api/v2/pokemon/${id}`; } // Hvert listeelement har en url som ".../pokemon/25/". Vi plukker ut tallet // (id-en) fordi vi trenger den for å bygge bilde-URL-en. function idFromUrl(url: string): number { - const match = url.match(/\/pokemon\/(\d+)\//) - return match ? Number(match[1]) : 0 + const match = url.match(/\/pokemon\/(\d+)\//); + return match ? Number(match[1]) : 0; } export async function searchPokemon(query: string): Promise { - const list = await fetchPokemonList() - const normalized = query.trim().toLowerCase() + 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}`, - })) + 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}`, + })); } diff --git a/src/App.css b/src/App.css index a028c86..d27aa90 100644 --- a/src/App.css +++ b/src/App.css @@ -44,7 +44,9 @@ font: inherit; color: var(--text-h); background: var(--bg); - transition: border-color 0.15s ease, box-shadow 0.15s ease; + transition: + border-color 0.15s ease, + box-shadow 0.15s ease; } .search-form input:focus-visible { @@ -62,7 +64,9 @@ font: inherit; font-weight: 700; cursor: pointer; - transition: transform 0.1s ease, background 0.15s ease; + transition: + transform 0.1s ease, + background 0.15s ease; } .search-form button:hover:not(:disabled) { @@ -95,7 +99,10 @@ border: 2px solid var(--border); border-radius: var(--radius); background: var(--bg-soft); - transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease; + transition: + border-color 0.15s ease, + transform 0.15s ease, + box-shadow 0.15s ease; } .song:hover { @@ -143,7 +150,9 @@ line-height: 1; color: var(--border); cursor: pointer; - transition: transform 0.15s ease, color 0.15s ease; + transition: + transform 0.15s ease, + color 0.15s ease; } .favorite-toggle:hover { diff --git a/src/App.tsx b/src/App.tsx index 9445199..daaa1e3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,45 +1,47 @@ -import { useState } from 'react' -import type { FormEvent } from 'react' -import { searchPokemon } from './Api' -import type { Pokemon } from './Api' -import { useFavorites } from './useFavorites' -import FavoritesSidebar from './FavoritesSidebar' -import './App.css' +import { useState } from 'react'; +import type { FormEvent } from 'react'; +import { searchPokemon } from './Api'; +import type { Pokemon } from './Api'; +import { useFavorites } from './useFavorites'; +import FavoritesSidebar from './FavoritesSidebar'; +import './App.css'; function capitalize(text: string): string { - return text.charAt(0).toUpperCase() + text.slice(1) + return text.charAt(0).toUpperCase() + text.slice(1); } function App() { - const [query, setQuery] = useState('') - const [pokemon, setPokemon] = useState([]) - const [error, setError] = useState('') - const [isLoading, setIsLoading] = useState(false) - const { favorites, isFavorite, toggleFavorite } = useFavorites() + const [query, setQuery] = useState(''); + const [pokemon, setPokemon] = useState([]); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const { favorites, isFavorite, toggleFavorite } = useFavorites(); async function handleSearch(event: FormEvent) { - event.preventDefault() - const trimmedQuery = query.trim() + event.preventDefault(); + const trimmedQuery = query.trim(); if (!trimmedQuery) { - setPokemon([]) - setError('Skriv inn et navn (eller en del av et navn) først.') - return + setPokemon([]); + setError('Skriv inn et navn (eller en del av et navn) først.'); + return; } - setIsLoading(true) - setError('') + setIsLoading(true); + setError(''); try { - const results = await searchPokemon(trimmedQuery) - setPokemon(results) + const results = await searchPokemon(trimmedQuery); + setPokemon(results); if (results.length === 0) { - setError(`Fant ingen Pokémon som matcher «${trimmedQuery}».`) + setError(`Fant ingen Pokémon som matcher «${trimmedQuery}».`); } } catch (searchError) { - setPokemon([]) - setError(searchError instanceof Error ? searchError.message : 'Noe gikk galt.') + setPokemon([]); + setError( + searchError instanceof Error ? searchError.message : 'Noe gikk galt.' + ); } finally { - setIsLoading(false) + setIsLoading(false); } } @@ -49,7 +51,10 @@ function App() {

    PokéAPI

    Finn en Pokémon

    -

    Søk på navn, eller en del av et navn – for eksempel «char» eller «pikachu».

    +

    + Søk på navn, eller en del av et navn – for eksempel «char» eller + «pikachu». +

    { - event.currentTarget.style.visibility = 'hidden' + event.currentTarget.style.visibility = 'hidden'; }} /> @@ -103,7 +108,7 @@ function App() { - ) + ); } -export default App +export default App; diff --git a/src/FavoritesSidebar.tsx b/src/FavoritesSidebar.tsx index 5a24107..f4282ee 100644 --- a/src/FavoritesSidebar.tsx +++ b/src/FavoritesSidebar.tsx @@ -1,13 +1,13 @@ -import type { Pokemon } from './Api' +import type { Pokemon } from './Api'; function capitalize(text: string): string { - return text.charAt(0).toUpperCase() + text.slice(1) + return text.charAt(0).toUpperCase() + text.slice(1); } type FavoritesSidebarProps = { - favorites: Pokemon[] - onRemove: (pokemon: Pokemon) => void -} + favorites: Pokemon[]; + onRemove: (pokemon: Pokemon) => void; +}; // Viser de lagrede favorittene. Selve komponenten er den samme uansett // skjermstørrelse - det er CSS-en (.favorites-bar) som gjør den om fra en @@ -16,11 +16,15 @@ function FavoritesSidebar({ favorites, onRemove }: FavoritesSidebarProps) { return ( - ) + ); } -export default FavoritesSidebar +export default FavoritesSidebar; diff --git a/src/main.tsx b/src/main.tsx index bef5202..df655ea 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,10 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import App from './App.tsx'; createRoot(document.getElementById('root')!).render( - , -) + +); diff --git a/src/useFavorites.ts b/src/useFavorites.ts index 93c6413..d37983a 100644 --- a/src/useFavorites.ts +++ b/src/useFavorites.ts @@ -1,18 +1,18 @@ -import { useCallback, useEffect, useState } from 'react' -import type { Pokemon } from './Api' +import { useCallback, useEffect, useState } from 'react'; +import type { Pokemon } from './Api'; // localStorage-nøkkelen favorittene lagres under. Ligger her som en // konstant slik at nøkkelen kun defineres ett sted i kodebasen. -const STORAGE_KEY = 'pokedex-favorites' +const STORAGE_KEY = 'pokedex-favorites'; function readFavorites(): Pokemon[] { try { - const raw = localStorage.getItem(STORAGE_KEY) - return raw ? (JSON.parse(raw) as Pokemon[]) : [] + const raw = localStorage.getItem(STORAGE_KEY); + return raw ? (JSON.parse(raw) as Pokemon[]) : []; } catch { // Ugyldig JSON eller localStorage utilgjengelig (f.eks. privat // nettlesing i enkelte nettlesere) -> start med tom liste. - return [] + return []; } } @@ -20,31 +20,33 @@ function readFavorites(): Pokemon[] { // Favoritter skal overleve at siden lastes på nytt (i motsetning til // f.eks. søketeksten, som er en god kandidat for sessionStorage). export function useFavorites() { - const [favorites, setFavorites] = useState(() => readFavorites()) + const [favorites, setFavorites] = useState(() => readFavorites()); useEffect(() => { try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(favorites)) + localStorage.setItem(STORAGE_KEY, JSON.stringify(favorites)); } catch { // Lagring kan feile (f.eks. quota exceeded) - da fortsetter appen // bare med favorittene i minnet for resten av økten. } - }, [favorites]) + }, [favorites]); const isFavorite = useCallback( (id: number) => favorites.some((favorite) => favorite.id === id), - [favorites], - ) + [favorites] + ); const toggleFavorite = useCallback((pokemon: Pokemon) => { setFavorites((current) => { - const alreadyFavorite = current.some((favorite) => favorite.id === pokemon.id) + const alreadyFavorite = current.some( + (favorite) => favorite.id === pokemon.id + ); if (alreadyFavorite) { - return current.filter((favorite) => favorite.id !== pokemon.id) + return current.filter((favorite) => favorite.id !== pokemon.id); } - return [...current, pokemon] - }) - }, []) + return [...current, pokemon]; + }); + }, []); - return { favorites, isFavorite, toggleFavorite } + return { favorites, isFavorite, toggleFavorite }; } diff --git a/vite.config.ts b/vite.config.ts index 7dd9b17..1ba205c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,10 @@ -import react, { reactCompilerPreset } from '@vitejs/plugin-react' -import babel from '@rolldown/plugin-babel' -import { defineConfig } from 'vite' +import react, { reactCompilerPreset } from '@vitejs/plugin-react'; +import babel from '@rolldown/plugin-babel'; +import { defineConfig } from 'vite'; // https://vite.dev/config/ export default defineConfig({ - plugins: [ - react(), - babel({ presets: [reactCompilerPreset()] }), - ], + plugins: [react(), babel({ presets: [reactCompilerPreset()] })], server: { // Tillat at dev-serveren nås via en tunnel-URL (f.eks. trycloudflare.com). // Greit for et lokalt læringsprosjekt. @@ -15,4 +12,4 @@ export default defineConfig({ }, base: '/project1', -}) +});