3. Single Page Applications
3. Single Page Applications
Tijdens deze oefensessie bouw je een applicatie waarmee je de hoofdsteden van verschillende landen kan inoefenen. Tijdens deze oefeningenreeks oefen je op:
- De concepten uit les 1 & 2
- Routing via react-router
- Het gebruik van third-party UI libraries
- Het raadplegen van documentatie om relevante informatie te vinden
Voorbereiding
Maak een nieuw project aan en zorg ervoor dat alle nodige pakketten geïnstalleerd zijn. Je moet tijdens deze oefeningenreeks gebruik maken van routing met React Router en een layout bouwen met behulp van Tailwind en shadcn/ui.
Verwijder, net zoals tijdens de eerste oefensessie, alle inhoud uit de src map. Je begint van een leeg project. Maak alvast een nieuwe main.tsx aan en voeg daarna de startcode toe aan je project.
Startbestanden
De startcode maakt gebruikt van onderstaande bibliotheken, installeer deze.
Info
De countries-capitals bibliotheek is niet ideal, voor verschillende hoofdsteden wordt er een foute naam getoond. Aangezien dit aan de bibliotheek ligt en het doel is om React onder de knie te krijgen, mag je dit negeren.
Routing
De applicatie bevat 3 pagina’s. Maak voor elke pagina een nieuwe component. De pagina’s zijn: Home, Game, Highscores.
Voorzie routing voor alle pagina's en subpagina's, gebruikt onderstaande componentenboom om je routing op te bouwen. De tekst bij de pijlen geeft het pad aan (relatief ten opzichte van de parent). De groene kaders duiden componenten/pagina's (er is geen verschil tussen de twee) aan. Als er een dubbelpunt in het pad staat, duid dit een navigatieparameter aan.
Oefening 1: Home Page
Bouw onderstaande pagina na. Zorg dat de links werken en dat de routing geïmplementeerd wordt via React Router. De eerste link in de navbar verwijst naar de root route.
De knoppen in de twee kolommen verwijzen naar dezelfde plaats als de links in de navigatiebalk. De kolom layout is gemaakt met behulp van de Tailwind Flex utilities.
Bouw de routes zo op, dat de bijhorende componenten getoond worden als je op de links klikt (voorlopig zijn deze componenten nog leeg).

Oefening 2: Game Page
Gebruik tijdens het uitwerken van deze pagina en bijhorende subpagina's de capitalsAPI.ts en highscoresAPI.ts bestanden. In deze bestanden vind je een aantal functies met commentaar, je zult dus zelf op zoek moeten gaan naar de gepaste functies om een bepaalde functionaliteit uit te werken.
Game component
Begin met de component Game uit te bouwen. Deze component toont altijd, ongeacht de opgevraagde kind-route, de titel "Play the game!" en een tekst die weergeeft of er al dan niet een regio geselecteerd is. In het geval er een regio gekozen is ook wat deze regio is. De gekozen regio wordt uitgelezen uit de navigatieparameters.
Hieronder de tekst die in beide situaties weergegeven moet worden.
- You've chosen to practice the capitals in REGION, click here to choose another region.
- Please select a region before playing the game.
Vervolgens wordt de inhoud van de kind-routes van het pad /game getoond, dit kan de ChooseRegion of Play component zijn. Zorgt dat je de juist component rendert met één lijn code in de Game component.
Als er nog geen regio geselecteerd is, redirect de Game component automatisch naar het pad /game/region. Let op, het is hier heel eenvoudig om een oneindige redirect cycle te creëren. Via de useLocation hook kan je dit probleem oplossen.
Regio kiezen
De ChooseRegion component toont, aan de hand van onderstaande ListGroup en ListGroupItem componenten, een overzicht van alle beschikbare regio's. Merk op dat je de Separator component nog moet installeren.
import type {FunctionComponent, MouseEventHandler, PropsWithChildren} from 'react'
import {Children} from 'react'
import {Separator} from '@/components/ui/separator.tsx'
import {cn} from '@/lib/utils.ts'
export const ListGroup: FunctionComponent<PropsWithChildren> = ({children}) => {
const lastIndex = Children.count(children) - 1
return (
<div className="my-4 border-2 rounded">
{Children.map(children, (child, i) => (
<>
{child}
{i !== lastIndex && <Separator />}
</>
))}
</div>
)
}
interface ListGroupItemProps extends PropsWithChildren {
// Of er een achtergrond getoond moet worden op het item.
selected?: boolean
// Klik event
onClick?: MouseEventHandler<HTMLDivElement>
// Of er een achtergrond en pointer getoond moeten worden
// als er over het item gehoverd wordt.
action?: boolean
}
export const ListGroupItem: FunctionComponent<ListGroupItemProps> = ({children, action, selected, onClick}) => {
return (
<div className={cn('rounded', action && 'hover:bg-accent cursor-pointer')}>
<div className={cn('py-2 px-4', {'bg-accent': selected})} onClick={onClick}>
{children}
</div>
</div>
)
}Als de gebruiker een regio aanduidt, wordt er automatisch geredirect naar het /game/play/[geselecteerde regio hier] pad.
Play component
De Play component bevat drie delen, de configuratie, de vragen en het resultaat. We beginnen met de configuratie uit te bouwen.
Game menu
Standaard begint een spel met 5 vragen, voor de meeste regio's zijn er echter meer dan 5 landen beschikbaar (regio's met minder dan 5 beschikbare landen worden in de API weggefilterd). De gebruiker kan ervoor kiezen om het aantal vragen te verhogen of te verlagen. Dit kan op twee manieren, ten eerste kan gebruik gemaakt worden van een + en - knop, ten tweede kan de hoeveelheid aangepast worden door middel van een Slider component. Het is vanzelfsprekend dat de range input en de knoppen gesynchroniseerd worden en dat het onmogelijk is om meer dan het beschikbare aantal of minder dan 5 vragen te selecteren.
Voor gebieden waar er exact 5 landen beschikbaar zijn, worden de formulierelementen niet getoond. Je kan dit testen door de regio "Australia and New Zealand" te selecteren.

Verder is er ook een knop aanwezig waarmee het spel opnieuw gestart kan worden. Voordat op deze knop gedrukt is, heeft het aantal geselecteerde vragen geen effect op het volgende deel van de oefening. De knop kan altijd ingedrukt worden, je hoeft geen controles toe te voegen die garanderen dat er geen spel bezig is.
Om bovenstaande layout te bouwen is gebruik gemaakt van de Tailwind Flex utilities.
Vragen tonen
Bouw een component Question die gebruikt kan worden om één vraag weer te geven. Naast de huidige vraag wordt ook de vooruitgang getoond en het aantal correct beantwoorde vragen in dit spel.
Zodra er een antwoord geselecteerd is wordt de volgende vraag getoond en wordt de vooruitgang en eventueel de score aangepast.
De layout in onderstaand screenshot is gebouwd met behulp van de Card componenten uit shadcn/ui en de Tailwind Typography utilities.

Highscores bewaren
Als het spel afgelopen is wordt de Result component getoond, deze component toont je score en een formulier waarmee je de highscore kan bewaren. Gebruik hiervoor de Input component en niet de Form component. Deze laatste component is bedoeld om complexere formulieren met foutcontrole, foutmeldingen, ... te bouwen.
De naam in het formulier is standaard ingesteld op de laatst gebruikte naam (op te halen via de API-code). De knop "Add to highscores!" is gedeactiveerd zolang het formulier leeg is.

Nadat een highscore is toegevoegd, zie je onderstaand scherm, de link verwijst naar /highscores/[geselecteerde locatie].

Onderstaande video demonstreert de volledige werking van de Game component.
Oefening 3: Highscore Page
De highscore pagina kan op twee manieren bezocht worden:
De gebruiker drukt op de link nadat een highscore bewaard is. In dat geval wordt de gebruiker naar /highscores/:chosenRegion gebracht. Deze URL bevat een parameter die gebruikt wordt om automatisch de regio te selecteren waarin de gebruiker zijn laatste spel gespeeld heeft.
De gebruiker drukt op de Highscores link in de navigatiebalk. In dit geval wordt de gebruiker naar /highscores gebracht. Er is geen parameter aanwezig in de URL en dus moet de gebruiker eerst een regio selecteren voordat er highscores zichtbaar zijn.
Beide links brengen de gebruiker naar dezelfde pagina. De Highscore component heeft twee verschillende layouts:
Voor de breakpoints x-small en small wordt een accordion gebruikt.
Voor alle andere breakpoints wordt een 2-kolom layout gebruikt, gebouwd met het grid-systeem.
Beide layouts worden hieronder besproken, het moet mogelijk zijn om te wisselen tussen de twee layouts, door de grootte van het venster aan te passen, zonder dat de geselecteerde regio verloren gaat. Beide layouts duiden de naam aan die het laatst gebruikt is om een highscore toe te voegen.
Om de zichtbaarheid van de layouts te bepalen kan je gebruik maken van volgend skelet.
<div className="sm:block md:hidden">
{/* ACCORDION */}
</div>
<div className="hidden md:block">
{/* COLUMN LAYOUT */}
</div>Layout 1: Accordion
Om deze layout te bouwen maak je gebruik van de shadcn/ui Accordion component.

Layout 2: Twee kolommen
De tweede layout bevat twee kolommen, gebouwd met de Tailwind Grid utilities. De eerste kolom 1/3 van de beschikbare ruimte in beslag, de andere kolom 2/3.
Door beide kolommen kan individueel gescrold worden. Om dit te implementeren moet de hoogte van de component vast gezet worden, hiervoor kan je de Tailwind klasse h-[80vh] gebruiken. Vervolgens kan je elke kolom dan omringen in een ScrollArea component.
