diff --git a/README.md b/README.md index 3c7db6d..7219884 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,180 @@ -# T27-Project-2 +# T27-Project-2 – Handlelappen -Repository for team IT2810-H26-T27 in IT2810-H26. +Repository for team IT2810-H26-T27 i IT2810-H26. -## Working in this repository +> **Status: Første underveisinnlevering.** Dette er en spesifikasjon og et første designutkast. Ingenting er implementert ennå, og alt kan endres. Vi tar gjerne imot tilbakemeldinger på idé, data, flyt og design. -`main` is protected — you cannot push to it directly. Work on a branch and -open a pull request: +## Idé + +Handlelappen er en digital handleliste. Du søker opp dagligvarer eller blar i kategorier, legger varer på lappen og ser hva handelen omtrent vil koste. I butikken starter du en handletur og krysser av varene etter hvert. Lister du har handlet før, kan du bruke på nytt, så du slipper å skrive alt igjen. + +**Hvorfor ikke en nettbutikk?** Først tenkte vi på noe som ligner Oda, der man handler varer direkte. Vi droppet det fordi vi ikke kan koble oss på ekte betaling, lager eller levering. Da kunne de viktigste funksjonene verken testes eller vises på en realistisk måte. En handleliste kan vi bygge og teste fullt ut, og den bruker likevel ekte varer og priser. + +## Brukerflyt + +![Skjermflyt](docs/brukerflyt.png) + +**Lage og bruke en handleliste (hovedflyt)** + +1. Brukeren åpner appen. +2. Brukeren søker etter varer eller velger en kategori. +3. Brukeren åpner produktsiden. +4. Varen legges på handlelisten. +5. Brukeren åpner den aktive handlelisten. +6. Brukeren starter handleturen. +7. Varer krysses av underveis. +8. Handleturen markeres som ferdig, og lista flyttes til tidligere handlelister. + +**Gjenbruke en tidligere liste** + +1. Brukeren åpner menyen. +2. Brukeren går til tidligere handlelister. +3. Brukeren velger en liste. +4. «Bruk på nytt» lager en ny aktiv liste med de samme varene og dagens priser. +5. Brukeren starter en ny handletur. + +## Design (første utkast) + +Designet er laget i Figma. Disse skjermbildene er et første utkast og kan endre seg. + +### Hjem og søk +Forsiden har søkefelt, kategorier og den aktive lappen. På søkesiden kan man filtrere på kategori, pris og merking (økologisk, laktosefri, norskprodusert) og sortere resultatene. Varer legges rett på lappen fra resultatlista, og lappen vises ved siden av. + +![Hjem og søk](docs/design/hjem-og-sok.png) + +### Produktside og meny +Produktsiden viser pris, kilopris, ingredienser, allergener og næringsinnhold. Her kan man endre antall og legge til et notat, for eksempel «skivet, hvis de har». Menyen gir tilgang til alle delene av appen. + +![Produktside og meny](docs/design/produkt-og-meny.png) + +### Mine handlelister og aktiv liste +Man kan ha flere aktive lister, og den man bruker nå er merket. På den aktive lista kan man endre antall, fjerne varer, sortere etter butikkavdeling og se estimert sum. + +![Handlelister](docs/design/handlelister.png) + +### Tidligere handlelister +Ferdige lister er gruppert etter måned. Man kan søke i dem og laste inn eldre lister. Fra en tidligere liste kan man bruke den på nytt eller legge varene på den aktive lappen. + +![Tidligere lister](docs/design/tidligere-lister.png) + +### Handletur (mobil) +Handleturen er laget for mobil, siden den brukes i butikken. Varene er gruppert etter avdeling, og en fremdriftslinje viser hvor mange som er krysset av. + +![Handletur på mobil](docs/design/handletur-mobil.png) + +## Data + +**Kilde:** [Kassalapp API](https://kassal.app/api), et API med norske dagligvarer og priser fra de fleste kjedene, blant annet Rema 1000, Kiwi, Meny, Coop, Joker, Spar og Bunnpris. + +Feltene vi har tenkt å bruke: + +- navn, merke, leverandør, vekt og bilde +- strekkode (EAN) +- pris og enhetspris (kr/kg eller kr/l) +- ingredienser, allergener og næringsinnhold +- kategori og merking, der API-et har det + +**Hvordan vi bruker API-et:** Frontend snakker aldri direkte med Kassalapp. Et importskript på backend henter varene og lagrer dem i vår egen PostgreSQL-database på VM-en, og GraphQL-API-et vårt søker bare der. Grunnene er: + +- Oppgaven krever egen database på VM-en. +- Gratisnøkkelen tillater 60 kall i minuttet og maks 100 varer per kall. Det holder for en import som går sakte, men ikke for at hver bruker skal søke rett mot Kassalapp. +- Når dataene ligger hos oss, kan vi sortere og filtrere på hele datasettet. + +API-nøkkelen ligger i en `.env`-fil og sjekkes ikke inn i git. Gratisnøkkelen er for ikke-kommersiell bruk. Prisene er et estimat og oppdateres når vi kjører importen på nytt. + +## Datamodell (utkast) + +``` +Product (id, ean, name, brand, vendor, weight, weightUnit, imageUrl, ingredients, allergens, nutrition, labels, categoryId) +Price (productId, store, price, unitPrice, updatedAt) +Category (id, name) +ShoppingList (id, ownerId, name, status: ACTIVE | DONE, inUse, createdAt, updatedAt, completedAt) +ListItem (id, listId, productId, quantity, note, checked) +``` + +Handlelister, varer på lista, antall, notater og avkrysninger er de brukergenererte dataene. Appen har ingen innlogging. Hver nettleser får en anonym bruker-ID som lagres lokalt og sendes med til backend. + +## GraphQL-API (utkast) + +```graphql +type Query { + products(search: String, filter: ProductFilter, sort: ProductSort, first: Int!, after: String): ProductConnection! + product(id: ID!): Product + categories: [Category!]! + shoppingLists(status: ListStatus, search: String, first: Int, after: String): ShoppingListConnection! + shoppingList(id: ID!): ShoppingList +} + +type Mutation { + createShoppingList(name: String!): ShoppingList! + setListInUse(listId: ID!): ShoppingList! + addToList(listId: ID!, productId: ID!, quantity: Int = 1): ListItem! + updateListItem(itemId: ID!, quantity: Int, note: String, checked: Boolean): ListItem! + removeFromList(itemId: ID!): ShoppingList! + finishShopping(listId: ID!): ShoppingList! + reuseList(listId: ID!): ShoppingList! # ny aktiv liste med samme varer + copyItemsToList(fromListId: ID!, toListId: ID!): ShoppingList! + deleteShoppingList(listId: ID!): Boolean! +} + +input ProductFilter { categoryIds: [ID!], priceMin: Float, priceMax: Float, labels: [String!] } +enum ProductSort { RELEVANCE, NAME_ASC, NAME_DESC, PRICE_ASC, PRICE_DESC, UNIT_PRICE_ASC } +``` + +Lister med mange elementer, som søkeresultater og tidligere handlelister, returnerer én side om gangen med totalt antall og en markør (cursor) for neste side. + +## Hvordan vi oppfyller kravene + +| Krav | Planlagt løsning | +| --- | --- | +| Søk | Søkefelt med knapp og debounce, så vi ikke sender et kall for hver tast. Søket skiller ikke mellom store og små bokstaver og håndterer æ, ø og å. Man kan også bla i kategorier. | +| Store resultatsett | Paginering på backend. Klienten henter én side av gangen og aldri hele datasettet. Tidligere lister lastes på samme måte med «Last inn eldre lister». | +| Detaljer | Egen produktside med pris, kilopris, ingredienser, allergener og næringsinnhold. | +| Sortering og filtrering | Gjøres i databasen, så det gjelder hele resultatsettet og ikke bare det som er lastet inn. Viste filtre har antall treff, og man kan nullstille dem. | +| Brukergenererte data | Handlelister, antall, notater og avkrysninger lagres i databasen på VM-en. | +| Tilgjengelighet | Semantisk HTML, tastaturnavigasjon, synlig fokus, labels på skjemafelt og god nok kontrast (WCAG 2.1 AA). Avkrysning vises med mer enn farge (hake og overstreking). Vi tester med Lighthouse, axe og skjermleser. | +| Bærekraft | Bilder er skrudd av i resultatlista som standard («Vis bilder»), og når de vises, er de små og lastes lazy. Data hentes fra Kassalapp med en import og ikke ved hvert søk. Spørringene henter bare feltene som vises, og vi bruker få og lette biblioteker. | + +## Teknologi + +| Del | Valg | Hvorfor | +| --- | --- | --- | +| Frontend | React, TypeScript og Vite | Kravet i oppgaven | +| State | Apollo Client | Cache for data fra GraphQL og lokal state som filtervalg og aktiv liste | +| Backend | Node, TypeScript og Apollo Server | Egen GraphQL-backend på port 3001 | +| Database | PostgreSQL direkte på VM-en | Relasjonelle data (varer, priser, lister) og godt søk og indekser | +| Kvalitet | ESLint, Prettier, Vitest og Playwright | Lint, komponenttester, ende-til-ende-tester og tester av API-et | + +## Plan for innleveringene + +1. Spesifikasjon, skjermflyt og designutkast (denne README-en). +2. PostgreSQL og backend på VM-en, import fra Kassalapp, og søk og visning av varer i klienten. +3. Komplett beta med paginering, filtrering, sortering, handlelister og handletur, pluss tilgjengelighet og bærekraft. +4. Tester, opprydding og dokumentasjon. + +## Bruk av KI + +README-en til første innlevering er skrevet med hjelp fra Claude (KI), basert på gruppas egen skjermflyt, design, møtereferater og valg av datakilde. Gruppa har gått gjennom innholdet. Vi fortsetter å dokumentere bruken av KI her gjennom prosjektet. + +## Kjøre prosjektet lokalt ```bash -git checkout -b my-feature -git push -u origin my-feature +cd frontend +npm install +npm run dev ``` -Before a pull request can be merged: +Instruksjoner for backend og database kommer til andre innlevering. + +## Arbeidsflyt + +1. **Lag en issue** for hver oppgave, f.eks. `#12 Lage produktsøk`. +2. **Assign deg selv** til issuen før du begynner, så unngår vi dobbeltarbeid. +3. **Lag en branch** med issuenummeret i navnet, på formen `type/nummer-beskrivelse`, f.eks. `feature/12-product-search` eller `docs/4-readme`. +4. **Gjør små commits** med beskrivende meldinger, f.eks. `feat: add search component`. +5. **Lag en pull request mot `dev`**, og skriv `Closes #12` i beskrivelsen. +6. **Få review:** minst én annen i gruppa ser over koden. Alle kommentarer må løses, og nye commits fjerner godkjenningen. +7. **Merge til `dev`** etter godkjenning. +8. **Merge `dev` til `main`** når en versjon er klar, f.eks. før hver innlevering. -1. **A member of IT2810-H26-T27 must approve it.** You cannot approve your own pull - request, and approvals from staff do not count — you review each other's - code. -2. **Every review comment must be marked resolved.** Reply to each one, fix it - or explain why not, then click *Resolve conversation*. -3. **Pushing new commits dismisses the approval**, so if you change something - after being approved, ask for another review. This is deliberate: what was - approved is what gets merged. \ No newline at end of file +`main` er beskyttet, så ingen kan pushe direkte dit. diff --git a/docs/brukerflyt.png b/docs/brukerflyt.png new file mode 100644 index 0000000..90d342d Binary files /dev/null and b/docs/brukerflyt.png differ diff --git a/docs/design/handlelister.png b/docs/design/handlelister.png new file mode 100644 index 0000000..f157533 Binary files /dev/null and b/docs/design/handlelister.png differ diff --git a/docs/design/handletur-mobil.png b/docs/design/handletur-mobil.png new file mode 100644 index 0000000..2f17f65 Binary files /dev/null and b/docs/design/handletur-mobil.png differ diff --git a/docs/design/hjem-og-sok.png b/docs/design/hjem-og-sok.png new file mode 100644 index 0000000..432e894 Binary files /dev/null and b/docs/design/hjem-og-sok.png differ diff --git a/docs/design/produkt-og-meny.png b/docs/design/produkt-og-meny.png new file mode 100644 index 0000000..e5b56ed Binary files /dev/null and b/docs/design/produkt-og-meny.png differ diff --git a/docs/design/tidligere-lister.png b/docs/design/tidligere-lister.png new file mode 100644 index 0000000..757dbb3 Binary files /dev/null and b/docs/design/tidligere-lister.png differ