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. 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/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

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;