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
8 changes: 7 additions & 1 deletion client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import './App.css'
import { useRoute } from './hooks/useRoute'
import { ChallengesPage } from './pages/ChallengesPage'
import { HomePage } from './pages/HomePage'
import { MapPage } from './pages/MapPage'
import { UserPage } from './pages/UserPage'

const pages = { home: HomePage, map: MapPage, user: UserPage }
const pages = {
home: HomePage,
map: MapPage,
user: UserPage,
challenges: ChallengesPage,
}

function App() {
const Page = pages[useRoute()]
Expand Down
17 changes: 17 additions & 0 deletions client/src/api/challenges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { mockChallenges } from '../data/mockChallenges'
import type { Challenge } from '../types/challenge'

/** Simulerer nettverksforsinkelse så lastetilstander kan sees i grensesnittet. */
const LATENCY_MS = 200

/**
* Henter alle utfordringene som kan trekkes på hjulet.
*
* Når GraphQL-backenden er klar, byttes innholdet her ut med et kall mot
* serveren (med samme signatur).
*/
export async function fetchChallenges(): Promise<Challenge[]> {
await new Promise((resolve) => setTimeout(resolve, LATENCY_MS))

return mockChallenges
}
11 changes: 10 additions & 1 deletion client/src/api/places.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mockPlaces } from '../data/mockPlaces'
import type { PlacesPage } from '../types/place'
import type { Place, PlacesPage } from '../types/place'

/** Simulerer nettverksforsinkelse så lastetilstander kan sees i grensesnittet. */
const LATENCY_MS = 200
Expand All @@ -22,3 +22,12 @@ export async function fetchPlaces(
totalCount: mockPlaces.length,
}
}

/** Så mange steder hentes når en side trenger hele listen (kart og hjul). */
const ALL_LIMIT = 500

/** Henter alle steder. Byttes ut med egne spørringer når backenden er klar. */
export async function fetchAllPlaces(): Promise<Place[]> {
const page = await fetchPlaces(0, ALL_LIMIT)
return page.items
}
8 changes: 7 additions & 1 deletion client/src/components/MainNav.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { ReactNode } from 'react'
import { HOME_HREF, MAP_HREF, USER_HREF } from '../hooks/useRoute'
import {
CHALLENGES_HREF,
HOME_HREF,
MAP_HREF,
USER_HREF,
} from '../hooks/useRoute'

/** Felles tegning for ikonene: små, enkle strek-ikoner. */
function Icon({ children }: { children: ReactNode }) {
Expand Down Expand Up @@ -63,6 +68,7 @@ const links: {
},
{
label: 'Utfordringer',
href: CHALLENGES_HREF,
// Flagg
icon: (
<Icon>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/user/BadgeSlots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function BadgeSlots() {
))}
</ul>
<p className="profile-empty">
Badges får du ved å fullføre utfordringer. De dukker opp her.
Merker får du ved å fullføre utfordringer. De dukker opp her.
</p>
</>
)
Expand Down
10 changes: 10 additions & 0 deletions client/src/components/wheel/ChallengeResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Challenge } from '../../types/challenge'

/** Utfordringen hjulet stoppet på. */
export function ChallengeResult({ challenge }: { challenge: Challenge }) {
return (
<article className="result-body">
<p className="challenge-text">{challenge.description}</p>
</article>
)
}
24 changes: 24 additions & 0 deletions client/src/components/wheel/CombinedResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Challenge } from '../../types/challenge'
import type { Place } from '../../types/place'
import { ChallengeResult } from './ChallengeResult'
import { PlaceResult } from './PlaceResult'

type CombinedResultProps = { place: Place; challenge: Challenge; now: Date }

/** Sted og utfordring side om side, når begge hjulene er snurret samtidig. */
export function CombinedResult({ place, challenge, now }: CombinedResultProps) {
return (
<section className="combined-result">
<article>
<h3>Sted</h3>
<p className="combined-name">{place.name}</p>
<PlaceResult place={place} now={now} />
</article>
<article>
<h3>Utfordring</h3>
<p className="combined-name">{challenge.title}</p>
<ChallengeResult challenge={challenge} />
</article>
</section>
)
}
28 changes: 28 additions & 0 deletions client/src/components/wheel/PlaceResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Place } from '../../types/place'
import { OpeningStatus } from '../OpeningStatus'
import { PlaceImage } from '../PlaceImage'

/** Informasjon om stedet hjulet stoppet på. */
export function PlaceResult({ place, now }: { place: Place; now: Date }) {
return (
<article className="result-body">
<figure className="featured-figure">
<PlaceImage place={place} />
<figcaption className="type-badge">{place.type}</figcaption>
</figure>

<p>{place.description}</p>

<dl className="facts">
<dt>Åpningstid</dt>
<dd>
<OpeningStatus openingHours={place.openingHours} now={now} />
</dd>
<dt>Område</dt>
<dd>{place.area}</dd>
<dt>Adresse</dt>
<dd>{place.address}</dd>
</dl>
</article>
)
}
73 changes: 73 additions & 0 deletions client/src/components/wheel/ResultDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useEffect, useId, useRef } from 'react'
import type { ReactNode } from 'react'

type ResultDialogProps = {
isOpen: boolean
title: string
/** Skjuler overskriften, når innholdet har egne. */
hideTitle?: boolean
wide?: boolean
onClose: () => void
children: ReactNode
}

/** Et lite vindu med resultatet fra hjulet. Esc og X-knappen lukker det. */
export function ResultDialog({
isOpen,
title,
hideTitle,
wide,
onClose,
children,
}: ResultDialogProps) {
const dialogRef = useRef<HTMLDialogElement>(null)
const titleId = useId()

// Nettleserens egen dialog tar seg av fokus, Esc og mørk bakgrunn.
useEffect(() => {
const dialog = dialogRef.current
if (!dialog) return

if (isOpen && !dialog.open) dialog.showModal()
else if (!isOpen && dialog.open) dialog.close()
}, [isOpen])

return (
<dialog
ref={dialogRef}
className={wide ? 'result-dialog wide' : 'result-dialog'}
aria-labelledby={titleId}
onClose={onClose}
// Et trykk på bakgrunnen treffer selve dialogen, ikke innholdet.
onClick={(event) => {
if (event.target === event.currentTarget) onClose()
}}
>
<header className="result-dialog-header">
<h2 id={titleId} className={hideTitle ? 'visually-hidden' : undefined}>
{title}
</h2>
<button
type="button"
className="map-control close-button"
aria-label="Lukk"
title="Lukk"
onClick={onClose}
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.4"
strokeLinecap="round"
aria-hidden="true"
focusable="false"
>
<path d="M6 6l12 12M18 6 6 18" />
</svg>
</button>
</header>
{children}
</dialog>
)
}
Loading