Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
85 changes: 5 additions & 80 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,87 +1,12 @@
import { useState } from 'react'
import './App.css'
import { FeaturedPlace } from './components/FeaturedPlace'
import { PlaceList } from './components/PlaceList'
import { SiteHeader } from './components/SiteHeader'
import { useNow } from './hooks/useNow'
import { PAGE_SIZE, usePlaces } from './hooks/usePlaces'
import type { Place } from './types/place'
import { useRoute } from './hooks/useRoute'
import { HomePage } from './pages/HomePage'
import { MapPage } from './pages/MapPage'

function App() {
const now = useNow()
const {
places,
totalCount,
status,
hasMore,
canShowLess,
loadMore,
showLess,
} = usePlaces()
const route = useRoute()

// Trekkes én gang per sidelasting, slik at det fremhevede stedet er tilfeldig.
const [startFraction] = useState(Math.random)
// Lagrer hele stedet, så det blir stående selv om «Vis mindre» fjerner det
// fra listen.
const [selected, setSelected] = useState<Place | null>(null)

const featured: Place | undefined =
selected ??
places[Math.floor(startFraction * Math.min(places.length, PAGE_SIZE))]

/** Blar til forrige (-1) eller neste (1) sted, og starter om i endene. */
const step = (direction: -1 | 1) => {
if (!featured) return
const index = places.findIndex((place) => place.id === featured.id)

// Er stedet skjult av «Vis mindre», starter vi i enden av listen.
const next =
index === -1
? direction === 1
? 0
: places.length - 1
: (index + direction + places.length) % places.length

setSelected(places[next])
}

return (
<>
<SiteHeader />
<main className="page">
{status === 'loading' && <p role="status">Laster steder …</p>}
{status === 'error' && (
<p role="alert">
Kunne ikke hente steder. Prøv å laste siden på nytt.
</p>
)}

{featured && (
<>
<FeaturedPlace
place={featured}
now={now}
onPrevious={() => step(-1)}
onNext={() => step(1)}
/>
<PlaceList
places={places}
now={now}
featuredId={featured.id}
loadedCount={places.length}
totalCount={totalCount}
hasMore={hasMore}
canShowLess={canShowLess}
isLoadingMore={status === 'loadingMore'}
onSelect={setSelected}
onLoadMore={loadMore}
onShowLess={showLess}
/>
</>
)}
</main>
</>
)
return route === 'map' ? <MapPage /> : <HomePage />
}

export default App
34 changes: 34 additions & 0 deletions client/src/components/HelpButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { ReactNode } from 'react'

type HelpButtonProps = {
id: string
title: string
children: ReactNode
}

/** «?»-knappen øverst til høyre, som åpner en forklaring i en tekstboble. */
export function HelpButton({ id, title, children }: HelpButtonProps) {
return (
<>
<button
type="button"
className="icon-button help-button"
popoverTarget={id}
aria-label="Om nettsiden"
>
<span aria-hidden="true">?</span>
</button>

{/* Åpnes og lukkes av nettleserens innebygde popover-støtte. */}
<aside
id={id}
className="help-popover"
popover="auto"
aria-labelledby={`${id}-title`}
>
<h2 id={`${id}-title`}>{title}</h2>
<p>{children}</p>
</aside>
</>
)
}
13 changes: 10 additions & 3 deletions client/src/components/MainNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@ function Icon({ children }: { children: ReactNode }) {
}

/** Foreløpige lenker. «Hjem» er siden vi står på. */
const links: { label: string; icon: ReactNode; current?: boolean }[] = [
const links: {
label: string
icon: ReactNode
href?: string
current?: boolean
}[] = [
{
label: 'Utforsk',
href: '#/kart',
// Kompass
icon: (
<Icon>
Expand All @@ -44,6 +50,7 @@ const links: { label: string; icon: ReactNode; current?: boolean }[] = [
},
{
label: 'Hjem',
href: '#/',
current: true,
// Anker
icon: (
Expand Down Expand Up @@ -78,9 +85,9 @@ export function MainNav() {
return (
<nav className="main-nav" aria-label="Hovednavigasjon">
<ul>
{links.map(({ label, icon, current }) => (
{links.map(({ label, icon, href, current }) => (
<li key={label}>
<a href="#" aria-current={current ? 'page' : undefined}>
<a href={href ?? '#'} aria-current={current ? 'page' : undefined}>
{icon}
{label}
</a>
Expand Down
16 changes: 16 additions & 0 deletions client/src/components/MenuButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** Menyknappen øverst til venstre. Funksjonen kommer senere. */
export function MenuButton() {
return (
<button type="button" className="icon-button menu-button" aria-label="Meny">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path
d="M4 6h16M4 12h16M4 18h16"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</button>
)
}
59 changes: 11 additions & 48 deletions client/src/components/SiteHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,21 @@
import { HelpButton } from './HelpButton'
import { MainNav } from './MainNav'
import { MenuButton } from './MenuButton'
import { ShipIcon } from './ShipIcon'

function MenuIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path
d="M4 6h16M4 12h16M4 18h16"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)
}

export function SiteHeader() {
return (
<header className="site-header">
{/* Funksjonen til menyen kommer senere. */}
<button
type="button"
className="icon-button menu-button"
aria-label="Meny"
>
<MenuIcon />
</button>

<button
type="button"
className="icon-button help-button"
popoverTarget="help-popover"
aria-label="Om nettsiden"
>
<span aria-hidden="true">?</span>
</button>
<MenuButton />

{/* Åpnes og lukkes av nettleserens innebygde popover-støtte. */}
<aside
id="help-popover"
className="help-popover"
popover="auto"
aria-labelledby="help-title"
>
<h2 id="help-title">Slik fungerer Tapsailor</h2>
<p>
Tapsailor hjelper deg å finne barer, puber og andre utesteder i
Trondheim. Stedet til venstre er et tilfeldig forslag som endres hver
gang du laster inn siden. Bruk «Forrige» og «Neste» for å bla gjennom
stedene, eller trykk på et sted i listen til høyre for å fremheve det.
Trykk «Vis mer» nederst for å laste flere forslag, og «Vis mindre» for
å skjule dem igjen. Menyen og de andre sidene kommer snart.
</p>
</aside>
<HelpButton id="help-popover" title="Slik fungerer Tapsailor">
Tapsailor hjelper deg å finne barer, puber og andre utesteder i
Trondheim. Stedet til venstre er et tilfeldig forslag som endres hver
gang du laster inn siden. Bruk «Forrige» og «Neste» for å bla gjennom
stedene, eller trykk på et sted i listen til høyre for å fremheve det.
Trykk «Vis mer» nederst for å laste flere forslag, og «Vis mindre» for å
skjule dem igjen. Under «Utforsk» ser du stedene på et kart.
</HelpButton>

{/* Dekor: noen måker over havet, én gruppe på hver side av tittelen. */}
<svg
Expand Down
62 changes: 62 additions & 0 deletions client/src/components/map/MapPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { KeyboardEvent, Ref } from 'react'
import type { Place } from '../../types/place'
import { PlaceDetails } from './PlaceDetails'
import { PlaceSearch } from './PlaceSearch'

type MapPanelProps = {
ref?: Ref<HTMLElement>
places: Place[]
selected: Place | null
query: string
now: Date
isLoading: boolean
onQueryChange: (query: string) => void
onSelect: (id: string) => void
onBack: () => void
onHover: (id: string | null) => void
}

/**
* Ruten til venstre. Den har fast størrelse og viser enten søk og forslag
* eller informasjon om det valgte stedet.
*/
export function MapPanel({
ref,
places,
selected,
query,
now,
isLoading,
onQueryChange,
onSelect,
onBack,
onHover,
}: MapPanelProps) {
// Escape går tilbake til søket, som pilen øverst i ruten.
const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {
if (event.key === 'Escape' && selected) onBack()
}

return (
<aside
ref={ref}
className="map-panel"
aria-label="Søk og steder"
onKeyDown={handleKeyDown}
>
{selected ? (
<PlaceDetails place={selected} now={now} onBack={onBack} />
) : (
<PlaceSearch
places={places}
query={query}
now={now}
isLoading={isLoading}
onQueryChange={onQueryChange}
onSelect={onSelect}
onHover={onHover}
/>
)}
</aside>
)
}
Loading