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
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ privatpersoner i Trondheim. Bygget med React, TypeScript og Vite.
`nvm use`.
- npm 10 eller nyere (følger med Node).

`engines` i `package.json` angir minstekrav (Node 22.12 og npm 10), men npm
`engines` i `package.json` angir minstekrav (Node 22.22 og npm 10), men npm
stopper ikke installasjonen hvis versjonen er feil. Sjekk selv med
`node -v` og `npm -v`.

Expand Down Expand Up @@ -52,11 +52,30 @@ src/
assets/ Illustrasjoner til eksempelannonsene
styles/ Designvariabler og grunnstiler
components/ Gjenbrukbare komponenter, én mappe per komponent
pages/ Én mappe per side
hooks/ Små felles hooks, for eksempel usePageTitle
design/ Designreferanse, bare under utvikling
```

Komponenter i `components/` får data gjennom props og importerer ikke
`data/` direkte. Da kan datakilden byttes uten at visningene må skrives om.
`data/` direkte. Bare sidene i `pages/` henter data. Da kan datakilden byttes
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» |

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
hopp-lenke automatisk.

## Design

Expand Down
53 changes: 51 additions & 2 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,58 @@ Bildet har tom alt-tekst fordi tittelen rett under beskriver det samme.
```

Tittelen er en `h2`. Katalogen kan gjøre tittelen til en lenke til
detaljvisningen når den finnes.
detaljvisningen, for eksempel `/annonser/listing-001`.

### Button

Vanlig `<button>` med felles stil. `variant="primary"` (standard) brukes til
én hovedhandling per visning, `variant="secondary"` til andre handlinger.
Standard `type` er `"button"`, så bruk `type="submit"` i skjemaer. Alle vanlige
knappeattributter, som `onClick` og `disabled`, sendes videre.

```tsx
<Button type="submit">Send forespørsel</Button>
<Button variant="secondary" onClick={onCancel}>Avbryt</Button>
<Button disabled>Deaktivert</Button>
```

Ikke lag knapper som later som de lagrer noe. Før backend finnes, skal en
handling tydelig vise at den er en demo.

### TextField

Tekstfelt med synlig etikett som også blir feltets tilgjengelige navn.
`hint` og `error` kobles til feltet med `aria-describedby`, og `error` setter
`aria-invalid`. Alle vanlige input-attributter sendes videre.

```tsx
<TextField label="Tittel" required />
<TextField label="Område" hint="Bydel eller område i Trondheim." />
<TextField
label="Døgnpris i kroner"
type="number"
min={0}
error="Prisen kan ikke være negativ. Skriv 0 for gratis utlån."
/>
```

Feltet lager sin egen `id`. Nedtrekkslister og tekstområder lages med vanlige
HTML-elementer når skjemaet trenger dem, med samme stil som `TextField`.

### Layout

Felles sidemal for alle sider. Den har en hopp-lenke til hovedinnholdet,
header med navigasjon til «Katalog» og «Lei ut verktøy», `<main>` og en
footer som sier at dataene er eksempler. Aktiv side markeres med fet tekst
og understrek, i tillegg til `aria-current="page"`. Ved sidebytte flyttes
fokus til `<main>`.

Hver side setter fanetittel med `usePageTitle`:

```tsx
usePageTitle('Katalog'); // «Katalog – Verktøyutlån Trondheim»
```

## Ikke laget ennå

- Knapp, skjemafelt, sidemal og navigasjon kommer i egen oppgave.
- Mørk modus er ikke planlagt for første innlevering.
37 changes: 36 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"engines": {
"node": ">=22.12.0",
"node": ">=22.22.0",
"npm": ">=10"
},
"scripts": {
Expand All @@ -18,7 +18,8 @@
},
"dependencies": {
"react": "^19.3.0",
"react-dom": "^19.3.0"
"react-dom": "^19.3.0",
"react-router": "^8.4.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand Down
26 changes: 19 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { Route, Routes } from 'react-router';
import Layout from './components/Layout/Layout.tsx';
import CatalogPage from './pages/CatalogPage/CatalogPage.tsx';
import ListingDetailPage from './pages/ListingDetailPage/ListingDetailPage.tsx';
import NotFoundPage from './pages/NotFoundPage/NotFoundPage.tsx';
import RentOutPage from './pages/RentOutPage/RentOutPage.tsx';

/**
* Rutene i appen. Routeren selv ligger i main.tsx, slik at tester kan pakke
* App inn i en MemoryRouter.
*/
function App() {
return (
<main>
<h1>Verktøyutlån Trondheim</h1>
<p>
Her kommer en katalog der studenter og privatpersoner kan leie og låne
verktøy av hverandre. Siden er under utvikling.
</p>
</main>
<Routes>
<Route element={<Layout />}>
<Route index element={<CatalogPage />} />
<Route path="annonser/:listingId" element={<ListingDetailPage />} />
<Route path="lei-ut" element={<RentOutPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
);
}

Expand Down
38 changes: 38 additions & 0 deletions src/components/Button/Button.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: var(--tap-target);
padding: var(--space-2) var(--space-6);
border: 2px solid transparent;
border-radius: var(--radius-md);
font: inherit;
font-weight: var(--font-weight-bold);
cursor: pointer;
}

.primary {
background: var(--color-accent);
color: var(--color-on-accent);
}

.primary:hover:not(:disabled) {
background: var(--color-accent-hover);
}

.secondary {
background: var(--color-surface);
border-color: var(--color-border-strong);
color: var(--color-text);
}

.secondary:hover:not(:disabled) {
background: var(--color-surface-muted);
}

.button:disabled {
background: var(--color-surface-muted);
border-color: var(--color-border);
color: var(--color-text-muted);
cursor: not-allowed;
}
26 changes: 26 additions & 0 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ButtonHTMLAttributes } from 'react';
import styles from './Button.module.css';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** primary brukes til én hovedhandling per visning, secondary til resten. */
variant?: 'primary' | 'secondary';
}

/**
* Vanlig knapp med felles stil. Standard type er "button", slik at knappen
* ikke sender inn et skjema ved et uhell. Bruk type="submit" i skjemaer.
*/
function Button({
variant = 'primary',
type = 'button',
className,
...buttonProps
}: ButtonProps) {
const classNames = [styles.button, styles[variant], className]
.filter(Boolean)
.join(' ');

return <button type={type} className={classNames} {...buttonProps} />;
}

export default Button;
107 changes: 107 additions & 0 deletions src/components/Layout/Layout.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
}

/* Skjult til den får tastaturfokus, og vises da øverst til venstre. */
.skipLink {
position: absolute;
top: var(--space-2);
left: var(--space-2);
z-index: 10;
padding: var(--space-2) var(--space-4);
background: var(--color-surface);
border-radius: var(--radius-md);
transform: translateY(-200%);
}

.skipLink:focus {
transform: none;
}

.header {
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
}

.headerInner,
.main,
.footer {
width: 100%;
max-width: var(--content-width);
margin: 0 auto;
padding-inline: var(--space-4);
}

.headerInner {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--space-2) var(--space-6);
padding-block: var(--space-2);
}

.brand {
display: inline-flex;
align-items: center;
min-height: var(--tap-target);
color: var(--color-text);
font-size: var(--font-size-lg);
font-weight: var(--font-weight-bold);
text-decoration: none;
}

.navList {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin: 0;
padding: 0;
list-style: none;
}

.navLink {
display: inline-flex;
align-items: center;
min-height: var(--tap-target);
padding-inline: var(--space-3);
border-radius: var(--radius-md);
color: var(--color-text);
text-decoration: none;
}

.navLink:hover {
background: var(--color-surface-muted);
color: var(--color-text);
}

/* Aktiv side markeres med tykk understrek og fet tekst, ikke bare farge. */
.navLink[aria-current='page'] {
font-weight: var(--font-weight-bold);
text-decoration: underline;
text-decoration-thickness: 3px;
text-decoration-color: var(--color-accent);
text-underline-offset: 0.4em;
}

.main {
flex: 1;
padding-block: var(--space-8);
}

/* main får fokus programmatisk ved sidebytte, uten synlig ring. */
.main:focus {
outline: none;
}

.footer {
padding-block: var(--space-6);
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}

.footer p {
margin: 0;
}
Loading