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
188 changes: 172 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
`main` er beskyttet, så ingen kan pushe direkte dit.
Binary file added docs/brukerflyt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/design/handlelister.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/design/handletur-mobil.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/design/hjem-og-sok.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/design/produkt-og-meny.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/design/tidligere-lister.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.