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
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 25 additions & 5 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<ListingCard listing={listing} />
```

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
<ListingGrid listings={listings} />
```

Rutenettet er en `<ul>`, 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

Expand Down
33 changes: 33 additions & 0 deletions src/components/ListingCard/ListingCard.module.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.card {
/* Referansepunkt for lenkeflaten som dekker hele kortet. */
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
Expand All @@ -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%;
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 9 additions & 3 deletions src/components/ListingCard/ListingCard.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
Expand All @@ -28,7 +30,11 @@ function ListingCard({ listing }: ListingCardProps) {
<div className={styles.imagePlaceholder}>Ingen bilde</div>
)}
<div className={styles.body}>
<h2 className={styles.title}>{listing.title}</h2>
<h2 className={styles.title}>
<Link className={styles.link} to={`/annonser/${listing.id}`}>
{listing.title}
</Link>
</h2>
<p className={styles.meta}>
{categoryLabels[listing.category]} · {listing.area}
</p>
Expand Down
19 changes: 19 additions & 0 deletions src/components/ListingGrid/ListingGrid.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
26 changes: 26 additions & 0 deletions src/components/ListingGrid/ListingGrid.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ul className={styles.grid}>
{listings.map((listing) => (
<li key={listing.id} className={styles.item}>
<ListingCard listing={listing} />
</li>
))}
</ul>
);
}

export default ListingGrid;
6 changes: 0 additions & 6 deletions src/design/DesignPreview.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
8 changes: 2 additions & 6 deletions src/design/DesignPreview.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -130,11 +130,7 @@ function DesignPreview() {

<section className={styles.section} aria-labelledby="annonsekort">
<h2 id="annonsekort">Annonsekort</h2>
<div className={styles.cardGrid}>
{exampleListings.map((listing) => (
<ListingCard key={listing.id} listing={listing} />
))}
</div>
<ListingGrid listings={exampleListings} />
</section>
</main>
);
Expand Down
7 changes: 6 additions & 1 deletion src/design/main.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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(
<StrictMode>
<DesignPreview />
<MemoryRouter>
<DesignPreview />
</MemoryRouter>
</StrictMode>,
);
18 changes: 5 additions & 13 deletions src/pages/CatalogPage/CatalogPage.tsx
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -14,18 +15,9 @@ function CatalogPage() {
<h1>Finn verktøy</h1>
<p>
Lei eller lån verktøy av andre studenter og privatpersoner i Trondheim.
Her kommer katalogen med søk, filtre og sortering.
</p>

<h2>Eksempelannonser</h2>
<p>Midlertidig liste til katalogen er på plass.</p>
<ul>
{mockListings.map((listing) => (
<li key={listing.id}>
<Link to={`/annonser/${listing.id}`}>{listing.title}</Link>
</li>
))}
</ul>
<ListingGrid listings={mockListings} />
</>
);
}
Expand Down