From 41bd5bd948b28421130c66095b4fc6157e77e251 Mon Sep 17 00:00:00 2001 From: Andreas Gresseth Date: Wed, 30 Sep 2026 18:43:07 +0200 Subject: [PATCH 1/4] =?UTF-8?q?feat(src):=20gj=C3=B8r=20annonsekortet=20ti?= =?UTF-8?q?l=20en=20lenke=20til=20detaljvisningen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ListingCard/ListingCard.module.css | 33 +++++++++++++++++++ src/components/ListingCard/ListingCard.tsx | 12 +++++-- src/design/main.tsx | 7 +++- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/components/ListingCard/ListingCard.module.css b/src/components/ListingCard/ListingCard.module.css index daebee3..90446a9 100644 --- a/src/components/ListingCard/ListingCard.module.css +++ b/src/components/ListingCard/ListingCard.module.css @@ -1,4 +1,6 @@ .card { + /* Referansepunkt for lenkeflaten som dekker hele kortet. */ + position: relative; display: flex; flex-direction: column; overflow: hidden; @@ -7,6 +9,16 @@ border-radius: var(--radius-md); } +.card:hover { + border-color: var(--color-border-strong); +} + +/* Fokusringen tegnes rundt hele kortet, siden hele kortet er klikkbart. */ +.card:has(.link:focus-visible) { + outline: var(--focus-ring); + outline-offset: var(--focus-offset); +} + .image, .imagePlaceholder { width: 100%; @@ -41,6 +53,27 @@ overflow-wrap: anywhere; } +.link { + color: var(--color-text); + text-decoration: none; +} + +/* Strekker lenkeflaten over hele kortet. */ +.link::after { + content: ''; + position: absolute; + inset: 0; +} + +.link:focus-visible { + outline: none; +} + +.card:hover .link { + color: var(--color-accent); + text-decoration: underline; +} + .meta { margin: 0; color: var(--color-text-muted); diff --git a/src/components/ListingCard/ListingCard.tsx b/src/components/ListingCard/ListingCard.tsx index bbed0ae..2c18fac 100644 --- a/src/components/ListingCard/ListingCard.tsx +++ b/src/components/ListingCard/ListingCard.tsx @@ -1,3 +1,4 @@ +import { Link } from 'react-router'; import type { Listing } from '../../types/listing.ts'; import { categoryLabels } from '../../utils/listingFormat.ts'; import PriceTag from '../PriceTag/PriceTag.tsx'; @@ -8,8 +9,9 @@ interface ListingCardProps { } /** - * Visuell referanse for et annonsekort. Katalogen kan bygge videre på denne, - * for eksempel ved å gjøre tittelen til en lenke til detaljvisningen. + * Annonsekort som åpner detaljvisningen. Selve lenken ligger på tittelen, men + * dekker hele kortet med CSS, så skjermlesere bare leser tittelen som lenketekst + * og tastaturbrukere får ett tabulatorstopp per kort. */ function ListingCard({ listing }: ListingCardProps) { return ( @@ -28,7 +30,11 @@ function ListingCard({ listing }: ListingCardProps) {
Ingen bilde
)}
-

{listing.title}

+

+ + {listing.title} + +

{categoryLabels[listing.category]} · {listing.area}

diff --git a/src/design/main.tsx b/src/design/main.tsx index 31a28db..d42065f 100644 --- a/src/design/main.tsx +++ b/src/design/main.tsx @@ -1,5 +1,6 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { MemoryRouter } from 'react-router'; import '../styles/tokens.css'; import '../styles/base.css'; import DesignPreview from './DesignPreview.tsx'; @@ -10,8 +11,12 @@ if (!rootElement) { throw new Error('Fant ikke #root i design.html'); } +// Annonsekortene inneholder lenker, så forhåndsvisningen trenger en router. +// MemoryRouter holder lenkene inne i forhåndsvisningen. createRoot(rootElement).render( - + + + , ); From 3a864bd6df28f6b8a97780f5856463e3f0a569c2 Mon Sep 17 00:00:00 2001 From: Andreas Gresseth Date: Wed, 30 Sep 2026 18:47:04 +0200 Subject: [PATCH 2/4] feat(src): lag rutenett for annonsekort --- .../ListingGrid/ListingGrid.module.css | 19 ++++++++++++++ src/components/ListingGrid/ListingGrid.tsx | 26 +++++++++++++++++++ src/design/DesignPreview.module.css | 6 ----- src/design/DesignPreview.tsx | 8 ++---- 4 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 src/components/ListingGrid/ListingGrid.module.css create mode 100644 src/components/ListingGrid/ListingGrid.tsx diff --git a/src/components/ListingGrid/ListingGrid.module.css b/src/components/ListingGrid/ListingGrid.module.css new file mode 100644 index 0000000..b64e1c4 --- /dev/null +++ b/src/components/ListingGrid/ListingGrid.module.css @@ -0,0 +1,19 @@ +.grid { + display: grid; + /* min() hindrer at kolonnene blir bredere enn skjermen på smale mobiler. */ + grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr)); + gap: var(--space-6); + margin: 0; + padding: 0; + list-style: none; +} + +/* Lar kortet fylle hele raden, slik at kortene i samme rad blir like høye. */ +.item { + display: flex; + flex-direction: column; +} + +.item > * { + flex: 1; +} diff --git a/src/components/ListingGrid/ListingGrid.tsx b/src/components/ListingGrid/ListingGrid.tsx new file mode 100644 index 0000000..96ab233 --- /dev/null +++ b/src/components/ListingGrid/ListingGrid.tsx @@ -0,0 +1,26 @@ +import type { Listing } from '../../types/listing.ts'; +import ListingCard from '../ListingCard/ListingCard.tsx'; +import styles from './ListingGrid.module.css'; + +interface ListingGridProps { + /** Annonsene som skal vises, i den rekkefølgen de skal stå. */ + listings: Listing[]; +} + +/** + * Responsivt rutenett med annonsekort. Rutenettet er en liste, slik at + * skjermlesere kan fortelle hvor mange annonser som vises. + */ +function ListingGrid({ listings }: ListingGridProps) { + return ( +
    + {listings.map((listing) => ( +
  • + +
  • + ))} +
+ ); +} + +export default ListingGrid; diff --git a/src/design/DesignPreview.module.css b/src/design/DesignPreview.module.css index ba7cae1..921cbf9 100644 --- a/src/design/DesignPreview.module.css +++ b/src/design/DesignPreview.module.css @@ -77,9 +77,3 @@ flex-direction: column; gap: var(--space-6); } - -.cardGrid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); - gap: var(--space-6); -} diff --git a/src/design/DesignPreview.tsx b/src/design/DesignPreview.tsx index 9e806da..06a294f 100644 --- a/src/design/DesignPreview.tsx +++ b/src/design/DesignPreview.tsx @@ -1,5 +1,5 @@ import Button from '../components/Button/Button.tsx'; -import ListingCard from '../components/ListingCard/ListingCard.tsx'; +import ListingGrid from '../components/ListingGrid/ListingGrid.tsx'; import PriceTag from '../components/PriceTag/PriceTag.tsx'; import TextField from '../components/TextField/TextField.tsx'; import { mockListings } from '../data/mockListings.ts'; @@ -130,11 +130,7 @@ function DesignPreview() {

Annonsekort

-
- {exampleListings.map((listing) => ( - - ))} -
+
); From 49419182520b697282a60c70e820455397f38043 Mon Sep 17 00:00:00 2001 From: Andreas Gresseth Date: Wed, 30 Sep 2026 18:50:06 +0200 Subject: [PATCH 3/4] feat(pages): vis annonsekort i katalogen --- src/pages/CatalogPage/CatalogPage.tsx | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/pages/CatalogPage/CatalogPage.tsx b/src/pages/CatalogPage/CatalogPage.tsx index d31c47b..7b83bc3 100644 --- a/src/pages/CatalogPage/CatalogPage.tsx +++ b/src/pages/CatalogPage/CatalogPage.tsx @@ -1,10 +1,11 @@ -import { Link } from 'react-router'; +import ListingGrid from '../../components/ListingGrid/ListingGrid.tsx'; import { mockListings } from '../../data/mockListings.ts'; import { usePageTitle } from '../../hooks/usePageTitle.ts'; /** - * Plassholder for katalogen. Søk, filtre, sortering og annonsekort lages i - * egen oppgave. Lenkelisten gjør det mulig å åpne detaljvisningen imens. + * Katalog med alle annonsene. Siden henter dataene og sender dem videre som + * props, så bare denne filen må endres når mockdataene byttes ut med GraphQL. + * Søk, filtre og sortering lages i egne oppgaver. */ function CatalogPage() { usePageTitle('Katalog'); @@ -14,18 +15,9 @@ function CatalogPage() {

Finn verktøy

Lei eller lån verktøy av andre studenter og privatpersoner i Trondheim. - Her kommer katalogen med søk, filtre og sortering.

-

Eksempelannonser

-

Midlertidig liste til katalogen er på plass.

-
    - {mockListings.map((listing) => ( -
  • - {listing.title} -
  • - ))} -
+ ); } From 3bae1493833209a5b3c618068f5b1708efbe546f Mon Sep 17 00:00:00 2001 From: Andreas Gresseth Date: Wed, 30 Sep 2026 19:58:00 +0200 Subject: [PATCH 4/4] docs(docs): dokumenter katalog og annonsekort --- README.md | 18 +++++++++--------- docs/design.md | 30 +++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 2303283..4824a89 100644 --- a/README.md +++ b/README.md @@ -63,15 +63,15 @@ uten at visningene må skrives om. ## Sider og ruter -Rutene er definert i `src/App.tsx` med React Router. Alle sidene er -foreløpig plassholdere som fylles ut i egne oppgaver. - -| Rute | Side | Innhold nå | -| ---------------------- | ------------------- | ------------------------------------------ | -| `/` | `CatalogPage` | Plassholder og lenker til eksempelannonser | -| `/annonser/:listingId` | `ListingDetailPage` | Tittel, område og pris for annonsen | -| `/lei-ut` | `RentOutPage` | Plassholder for utleieskjema | -| Alle andre | `NotFoundPage` | «Fant ikke siden» | +Rutene er definert i `src/App.tsx` med React Router. Sidene som ikke er +ferdige, er plassholdere som fylles ut i egne oppgaver. + +| Rute | Side | Innhold nå | +| ---------------------- | ------------------- | ------------------------------------- | +| `/` | `CatalogPage` | Alle eksempelannonser som annonsekort | +| `/annonser/:listingId` | `ListingDetailPage` | Tittel, område og pris for annonsen | +| `/lei-ut` | `RentOutPage` | Plassholder for utleieskjema | +| Alle andre | `NotFoundPage` | «Fant ikke siden» | Ny side: lag en mappe i `src/pages/`, legg til en `Route` i `App.tsx` og kall `usePageTitle` i siden. Sidemalen (`Layout`) gir header, navigasjon og diff --git a/docs/design.md b/docs/design.md index 53fc73f..96938cd 100644 --- a/docs/design.md +++ b/docs/design.md @@ -103,16 +103,36 @@ bakgrunn, men teksten alene er nok til å skille dem. ### ListingCard -Visuell referanse for et annonsekort med bilde, tittel, kategori, område og -pris. Når `imageUrl` er `null`, vises en plassholder med teksten «Ingen bilde». -Bildet har tom alt-tekst fordi tittelen rett under beskriver det samme. +Annonsekort med bilde, tittel, kategori, område og pris. Når `imageUrl` er +`null`, vises en plassholder med teksten «Ingen bilde». Bildet har tom +alt-tekst fordi tittelen rett under beskriver det samme. ```tsx ``` -Tittelen er en `h2`. Katalogen kan gjøre tittelen til en lenke til -detaljvisningen, for eksempel `/annonser/listing-001`. +Tittelen er en `h2` med en lenke til detaljvisningen, for eksempel +`/annonser/listing-001`. Lenken dekker hele kortet med CSS, så hele kortet er +klikkbart. Likevel har hvert kort bare ett tabulatorstopp, og skjermlesere +leser bare tittelen som lenketekst. Fokusringen og hover-effekten tegnes rundt +hele kortet. + +Kortet bruker `Link` fra React Router og må derfor ligge inne i en router. +Designreferansen bruker `MemoryRouter` av samme grunn. + +### ListingGrid + +Responsivt rutenett med ett `ListingCard` per annonse. Annonsene vises i den +rekkefølgen de sendes inn, så søk, filtre og sortering gjøres før listen +sendes til rutenettet. + +```tsx + +``` + +Rutenettet er en `
    `, slik at skjermlesere kan si hvor mange annonser som +vises. Kolonnene er minst 16rem brede og fyller bredden. På smale skjermer blir +det én kolonne uten horisontal scrolling. Kortene i samme rad blir like høye. ### Button