From 660d867121ff74f725f8d68f51a4636c198743c2 Mon Sep 17 00:00:00 2001 From: tordtst Date: Wed, 30 Sep 2026 11:46:53 +0200 Subject: [PATCH 1/3] feat(app): legg til ruting, sidemal og plassholdersider Gi appen felles header, navigasjon og hopp-lenke, og egne ruter for katalog, detaljvisning og utleie slik at gruppen kan fylle ut hver side for seg. Fokus flyttes til hovedinnholdet ved sidebytte. React Router krever Node 22.22 eller nyere. Refs #6 Co-Authored-By: sebasti --- package-lock.json | 37 +++++- package.json | 5 +- src/App.tsx | 26 +++-- src/components/Layout/Layout.module.css | 107 ++++++++++++++++++ src/components/Layout/Layout.tsx | 67 +++++++++++ src/hooks/usePageTitle.ts | 13 +++ src/main.tsx | 7 +- src/pages/CatalogPage/CatalogPage.tsx | 33 ++++++ .../ListingDetailPage/ListingDetailPage.tsx | 45 ++++++++ src/pages/NotFoundPage/NotFoundPage.tsx | 18 +++ src/pages/RentOutPage/RentOutPage.tsx | 18 +++ 11 files changed, 365 insertions(+), 11 deletions(-) create mode 100644 src/components/Layout/Layout.module.css create mode 100644 src/components/Layout/Layout.tsx create mode 100644 src/hooks/usePageTitle.ts create mode 100644 src/pages/CatalogPage/CatalogPage.tsx create mode 100644 src/pages/ListingDetailPage/ListingDetailPage.tsx create mode 100644 src/pages/NotFoundPage/NotFoundPage.tsx create mode 100644 src/pages/RentOutPage/RentOutPage.tsx diff --git a/package-lock.json b/package-lock.json index 0c85b0d..cbee598 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "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", @@ -573,6 +574,12 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "node_modules/@remix-run/route-pattern": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/@remix-run/route-pattern/-/route-pattern-0.22.1.tgz", + "integrity": "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==", + "license": "MIT" + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.11", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", @@ -1301,6 +1308,12 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2425,6 +2438,28 @@ "react": "^19.3.0" } }, + "node_modules/react-router": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.4.0.tgz", + "integrity": "sha512-JtydAkBvU9UTEe5qNC+POhu+ZBNM7rlKY2IegwXc7Idj7xfRG16x5RZrw3mju+XlqBxfvb2ky9P3jUp9WyWC1g==", + "license": "MIT", + "dependencies": { + "@remix-run/route-pattern": "^0.22.1", + "cookie-es": "^3.1.1" + }, + "engines": { + "node": ">=22.22.0" + }, + "peerDependencies": { + "react": ">=19.2.7", + "react-dom": ">=19.2.7" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/rolldown": { "version": "1.2.11", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", diff --git a/package.json b/package.json index 4a639ab..ce2ee1c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "engines": { - "node": ">=22.12.0", + "node": ">=22.22.0", "npm": ">=10" }, "scripts": { @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 694665a..8ebcb26 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( -
-

Verktøyutlån Trondheim

-

- Her kommer en katalog der studenter og privatpersoner kan leie og låne - verktøy av hverandre. Siden er under utvikling. -

-
+ + }> + } /> + } /> + } /> + } /> + + ); } diff --git a/src/components/Layout/Layout.module.css b/src/components/Layout/Layout.module.css new file mode 100644 index 0000000..d584e23 --- /dev/null +++ b/src/components/Layout/Layout.module.css @@ -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; +} diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx new file mode 100644 index 0000000..d69e80b --- /dev/null +++ b/src/components/Layout/Layout.tsx @@ -0,0 +1,67 @@ +import { useEffect, useRef } from 'react'; +import { Link, NavLink, Outlet, useLocation } from 'react-router'; +import styles from './Layout.module.css'; + +/** Felles sidemal med hopp-lenke, header, navigasjon og hovedinnhold. */ +function Layout() { + const { pathname } = useLocation(); + const mainRef = useRef(null); + const previousPathname = useRef(pathname); + + // Ved sidebytte flyttes fokus til hovedinnholdet, slik at skjermlesere og + // tastaturbrukere starter på den nye siden. Sammenligningen med forrige sti + // hindrer fokusflytting ved første innlasting, også når StrictMode kjører + // effekten to ganger. + useEffect(() => { + if (previousPathname.current === pathname) { + return; + } + previousPathname.current = pathname; + mainRef.current?.focus(); + }, [pathname]); + + return ( +
+ + Hopp til hovedinnhold + + +
+
+ + Verktøyutlån Trondheim + + +
+
+ +
+ +
+ +
+

Prototype. Alle annonser er oppdiktede eksempler.

+
+
+ ); +} + +export default Layout; diff --git a/src/hooks/usePageTitle.ts b/src/hooks/usePageTitle.ts new file mode 100644 index 0000000..047ccef --- /dev/null +++ b/src/hooks/usePageTitle.ts @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; + +const SITE_NAME = 'Verktøyutlån Trondheim'; + +/** + * Setter fanetittelen, for eksempel «Katalog – Verktøyutlån Trondheim». + * Unik tittel per side hjelper skjermleserbrukere og faner i nettleseren. + */ +export function usePageTitle(pageTitle: string) { + useEffect(() => { + document.title = `${pageTitle} – ${SITE_NAME}`; + }, [pageTitle]); +} diff --git a/src/main.tsx b/src/main.tsx index cbc41b1..0fdc649 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,6 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router'; import App from './App.tsx'; import './styles/tokens.css'; import './styles/base.css'; @@ -11,8 +12,12 @@ if (!rootElement) { throw new Error('Fant ikke #root i index.html'); } +// basename følger Vites base-sti, slik at rutene også virker når appen senere +// publiseres under en undermappe som /project2. createRoot(rootElement).render( - + + + , ); diff --git a/src/pages/CatalogPage/CatalogPage.tsx b/src/pages/CatalogPage/CatalogPage.tsx new file mode 100644 index 0000000..d31c47b --- /dev/null +++ b/src/pages/CatalogPage/CatalogPage.tsx @@ -0,0 +1,33 @@ +import { Link } from 'react-router'; +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. + */ +function CatalogPage() { + usePageTitle('Katalog'); + + return ( + <> +

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} +
  • + ))} +
+ + ); +} + +export default CatalogPage; diff --git a/src/pages/ListingDetailPage/ListingDetailPage.tsx b/src/pages/ListingDetailPage/ListingDetailPage.tsx new file mode 100644 index 0000000..0e97456 --- /dev/null +++ b/src/pages/ListingDetailPage/ListingDetailPage.tsx @@ -0,0 +1,45 @@ +import { Link, useParams } from 'react-router'; +import PriceTag from '../../components/PriceTag/PriceTag.tsx'; +import { mockListings } from '../../data/mockListings.ts'; +import { usePageTitle } from '../../hooks/usePageTitle.ts'; + +/** + * Plassholder for detaljvisningen. Beskrivelse, tilstand, tilbehør og + * leieforespørsel lages i egen oppgave. + */ +function ListingDetailPage() { + const { listingId } = useParams(); + const listing = mockListings.find((item) => item.id === listingId); + usePageTitle(listing ? listing.title : 'Fant ikke annonsen'); + + if (!listing) { + return ( + <> +

Fant ikke annonsen

+

Annonsen finnes ikke, eller den er fjernet.

+

+ Tilbake til katalogen +

+ + ); + } + + return ( + <> +

+ Tilbake til katalogen +

+

{listing.title}

+

Område: {listing.area}

+

+ +

+

+ Her kommer detaljvisningen med bilde, beskrivelse, tilstand, tilbehør og + leieforespørsel. +

+ + ); +} + +export default ListingDetailPage; diff --git a/src/pages/NotFoundPage/NotFoundPage.tsx b/src/pages/NotFoundPage/NotFoundPage.tsx new file mode 100644 index 0000000..487ce95 --- /dev/null +++ b/src/pages/NotFoundPage/NotFoundPage.tsx @@ -0,0 +1,18 @@ +import { Link } from 'react-router'; +import { usePageTitle } from '../../hooks/usePageTitle.ts'; + +function NotFoundPage() { + usePageTitle('Fant ikke siden'); + + return ( + <> +

Fant ikke siden

+

Adressen finnes ikke.

+

+ Gå til katalogen +

+ + ); +} + +export default NotFoundPage; diff --git a/src/pages/RentOutPage/RentOutPage.tsx b/src/pages/RentOutPage/RentOutPage.tsx new file mode 100644 index 0000000..b81173a --- /dev/null +++ b/src/pages/RentOutPage/RentOutPage.tsx @@ -0,0 +1,18 @@ +import { usePageTitle } from '../../hooks/usePageTitle.ts'; + +/** Plassholder for skjemaet der brukere legger ut egne verktøy. */ +function RentOutPage() { + usePageTitle('Lei ut verktøy'); + + return ( + <> +

Lei ut verktøy

+

+ Her kommer skjemaet for å legge ut et verktøy med tittel, kategori, + beskrivelse, område og døgnpris. Pris 0 betyr gratis utlån. +

+ + ); +} + +export default RentOutPage; From eebe6cfc50c4b89af82114b31846e93d181c6b5c Mon Sep 17 00:00:00 2001 From: tordtst Date: Wed, 30 Sep 2026 11:46:53 +0200 Subject: [PATCH 2/3] feat(src): lag Button og TextField med tilgjengelige tilstander Gi skjemaer og handlinger felles knapper og felt med synlig fokus, koblede etiketter, feilmeldinger og deaktivert tilstand. Refs #6 Co-Authored-By: andretg --- src/components/Button/Button.module.css | 38 +++++++++++++ src/components/Button/Button.tsx | 26 +++++++++ src/components/TextField/TextField.module.css | 46 ++++++++++++++++ src/components/TextField/TextField.tsx | 53 +++++++++++++++++++ src/design/DesignPreview.module.css | 6 +++ src/design/DesignPreview.tsx | 34 ++++++++++++ 6 files changed, 203 insertions(+) create mode 100644 src/components/Button/Button.module.css create mode 100644 src/components/Button/Button.tsx create mode 100644 src/components/TextField/TextField.module.css create mode 100644 src/components/TextField/TextField.tsx diff --git a/src/components/Button/Button.module.css b/src/components/Button/Button.module.css new file mode 100644 index 0000000..8462bcc --- /dev/null +++ b/src/components/Button/Button.module.css @@ -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; +} diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx new file mode 100644 index 0000000..2ef4776 --- /dev/null +++ b/src/components/Button/Button.tsx @@ -0,0 +1,26 @@ +import type { ButtonHTMLAttributes } from 'react'; +import styles from './Button.module.css'; + +interface ButtonProps extends ButtonHTMLAttributes { + /** 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 + + + + + +
+

Skjemafelt

+
+ + + + +
+
+

Annonsekort

From 373ad83e8d76e9a0fd6bcad1b382f983cc66c41b Mon Sep 17 00:00:00 2001 From: tordtst Date: Wed, 30 Sep 2026 11:46:53 +0200 Subject: [PATCH 3/3] docs(docs): dokumenter sider, ruter og UI-komponenter Vis hvordan nye sider legges til, og hvordan Button, TextField og sidemalen brukes, slik at gruppen kan bygge videre likt. Refs #6 Co-Authored-By: birkjr --- README.md | 23 ++++++++++++++++++++-- docs/design.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 511f667..2303283 100644 --- a/README.md +++ b/README.md @@ -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`. @@ -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 diff --git a/docs/design.md b/docs/design.md index 860cd2f..53fc73f 100644 --- a/docs/design.md +++ b/docs/design.md @@ -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 ` + + +``` + +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 + + + +``` + +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», `
` 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 `
`. + +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.