Herhalingsoefening II
Herhalingsoefening II
Tijdens deze oefeningenreeks bouw je een soort social network, we zullen gebruikers de optie geven om zich aan te sluiten bij een bepaalde groep, daarnaast zal een gebruiker een private of publieke groep maken. We zullen verder ook de optie voorzien om posts te plaatsen en commentaren op deze post te plaatsen. Tenslotte implementeren we ook een chat functie. Deze chat functie zal one-to-one en groepschats ondersteunen. In deze oefeningenreeks oefen je op:
- Componenten
- State
- Lifting state
- Prop drilling
- Routing
- Styling
- Het gebruik van third-party UI libraries
- Het raadplegen van documentatie om relevante informatie te vinden
- Het gebruik van TanStack Query
- Het gebruik van Suspense
- Het gebruik van Context
- Het schrijven van custom hooks
Info
Dit is een heel uitgebreide oefening. Het is normaal als je deze niet volledig afgewerkt krijgt tijdens de daarvoor voorziene lessen. Deze oefening vraagt veel meer tijd dan we kunnen voorzien in de lessen.
Zoek tijdens deze les niet te lang naar CSS problemen. Als de UI ongeveer overeenkomt, is dit voldoende. Focus op de functionaliteit. Op het examen wordt ook niet gekeken naar CSS, behalve naar het correct aanmaken van styled-components en het inladen van CSS-bestanden.
Oefening 0: Voorbereiding
Download de startbestanden en werk hieruit verder. In de startbestanden vindt je enkele utility componenten, de API-code waarmee je data kan ophalen van de Supabase back-end, het .env bestand met de API keys die nodig zijn om met Supabase te connecteren en start code voor de algemene lay-out.
Voor deze oefening moet je styling implementeren met Bootstrap en styled components en routing via React Router. Installeer de nodige libraries alvast, daarnaast heb je ook de onderstaande libraries nodig.
pnpm add bootstrap-icons @supabase/supabase-js
Om gebruik te kunnen maken van de Bootstrap iconen bibliotheek moet onderstaand import statement toegevoegd worden aan main.jsx.
import 'bootstrap-icons/font/bootstrap-icons.css'
Oefening 1: Navigatie
In de startbestanden vind je het bestand App.jsx, deze component bevat een skelet voor de layout van de website. De eerste kolom die teruggegeven wordt door de App component moet een SideNav component getoond worden. De structuur van deze component is te zien in onderstaand screenshot. Nadat inloggen geïmplementeerd is moeten bepaalde elementen verborgen of getoond worden, voorlopig mogen alle 5 de items getoond worden.

De SideNav component toont een side-nav met 4 links, elk van deze links bestaat uit een icoontje (2 kolommen breed) en een titel (10 kolommen breed). De gebruiker kan zowel op het icoontje als op de titel klikken om naar een andere pagina te navigeren.
Omring de volledige SideNav met een Bootstrap Row component die via styled-components onderstaande CSS krijgt. Deze CSS zorgt ervoor dat er rond geselecteerde links geen kader komt te staan als deze in focus zitten.
*:focus {
outline: none;
}
Gebruik een component NavItem die één link voorstelt. Als het NavItem de titel van de website is ("Social Network"), dan wordt er een horizontale lijn getoond onder de titel en het icoontje, daarnaast wordt een <h3> gebruikt voor het icoontje en de titel. Alle andere NavItem componenten gebruiken een <h4>. Bekijk de Bootstrap Icons documentatie voor meer info over het gebruik van de icoontjes. Onderstaande lijst geeft weer welke links in elk element in de SideNav moet staan en welk icoon gebruikt wordt.
- Social Network
- Verwijst naar het pad '/', wat de component Home laad.
- Het gebruikte icoon is: bi-house-door-fill.
- {username}
- Verwijst naar het pad '/user', wat de component User laad.
- Het gebruikte icoon is: bi-person-fill.
- Log In
- Verwijst naar het pad '/login', wat de component Login laad.
- Het gebruikte icoon is: bi-box-arrow-in-right.
- Groups
- Verwijst naar het pad '/groups', wat de component Group laad.
- Het gebruikte icoon is: bi-people-fill.
- Chat
- Verwijst naar het pad 'chat', wat de component Chat laad.
- Het gebruikte icoon is: bi-chat-fill
Oefening 1.1 Active link styling
Maak gebruik van de juiste properties in de NavLink component om ervoor te zorgen dat de geselecteerde link de CSS-klasse 'text-muted' krijgt. Onderstaande video demonstreert de werking.
Oefening 1.2 Dichtklappen
De SideNav component moet dichtgeklapt kunnen worden, in dit geval worden enkel de icoontjes getoond. Om het dichtklappen te implementeren kan je de icoontjes bi-arrow-left en bi-arrow-right gebruiken.
Oefening 2: Thema keuze
Gebruik context om de themakeuze aan te passen. Om het thema aan te passen kan je, indien de applicatie het light thema moet gebruiken de klassen bg-dark text-light toevoegen aan de Container component die in de App component staat.
De themakeuze moet persistent gemaakt worden. Telkens dat deze aangepast wordt, moet de keuze weggeschreven worden naar de localstorage. Als de applicatie start, moet deze keuze uitgelezen worden en gebruikt worden om de Provider te initialiseren.
Oefening 3: Authenticatie
Info
We gebruiken in deze les email/password authenticatie in de plaats van de magic link uit de vorige les. We doen dit omdat we in een latere les testen willen schreven op deze oefeningen. Het is niet mogelijk om testen te schrijven op het inlogproces als dit via magic links werkt omdat de testing libraries natuurlijk geen toegang hebben tot je mail om automatisch op de magic link te klikken.
Voor deze les is email verificatie uitgezet. Je kan met eender welk e-mailadres inloggen en hoeft dit niet te bevestigen.
Voeg een formulier toe aan de login pagina. Dit formulier kan op 2 manieren bekeken worden, de login pagina kan gebruikt worden om in te loggen en om een account aan te maken. De gebruiker kan wisselen tussen deze 2 opties. In onderstaande screenshots is de onderstaande component gebruikt om de knoppen te tonen waarmee gewisseld kan worden tussen de 2 opties.
const NoStyleButton = styled.button`
background: inherit;
color: inherit;
border: none;
&:focus {
outline: none;
}
`
Standaard staat het formulier in sign in modus. Het formulier bestaat in dat geval uit een e-mailadres, wachtwoord en een log-in knop. Deze knop is gebouwd met behulp van de FormSubmitButtonWithLoading die te vinden is in de startbestanden.

Als het formulier in registreer modus staat, worden dezelfde formulierelementen getoond, maar wordt er ook een formulier getoond om de gebruiksnaam in te geven. Merk op dat de tekst op de submit knop aangepast wordt. Ook de knoppen bovenaan worden aangepast, de optie die niet geselecteerd is krijgt de CSS-klasse text-muted.

Oefening 3.1: Mutaties schrijven
In de startbestanden vind je de login en register functies, gebruik deze om een mutation te schrijven waarmee de gebruiker kan inloggen en registeren. De mutationFn krijgt 3 parameters, het email-adres, het wachtwoord en eventueel de username. Als de username undefined is, wordt de gebruiker ingelogd, anders wordt de gebruiker geregistreerd. Je moet inloggen en registreren dus implementeren met één mutation.
Oefening 3.2: UI Afwerken
Je kan voor de loading property van de FormSubmitButtonWithLoading gebruik maken van de isLoading variable die teruggegeven wordt door de useMutation hook.
Voeg tenslotte nog een ResponseMessage component, die te vinden is in de startbestanden, toe boven de submit knop. Als er tijdens de mutatie iets foutgelopen is wordt de foutboodschap die door mutatie teruggegeven is getoond in deze component. Als alles goed verlopen is wordt er een gepaste boodschap getoond.
Oefening 3.3: useGetProfile & useProfile hooks
Schrijf een nieuwe query hook useGetProfile die gebruik maakt van de reeds voorziene functie fetchProfile methode om het profiel van de ingelogde gebruiker op te halen. Deze query wordt nooit automatisch als stale beschouwd en wordt nooit uit de cache verwijderd.
Pas de mutation die je in vraag 3.1 geschreven hebt vervolgens aan zodat deze, bij een succesvolle registratie, de query invalideert zodat de nieuwe profielgegevens opgehaald worden.
Gebruik useGetProfile hook vervolgens om een nieuwe hook useProfile te schrijven die 3 dingen teruggeef. Ten eerste wordt de profielinformatie van de ingelogde gebruiker weergegeven. De overige twee returnwaarden zijn twee boolean, isAuthenticated en isNotAuthenticated. Een gebruiker wordt als geauthenticeerd beschouwd als het id uit het profiel gelezen kan worden en als er in de query niets is foutgelopen.
Oefening 3.4: Redirects
Gebruik de useProfile hook om de gebruiker te redirecten naar /groups zodra het inloggen gelukt is, als je alle hooks correct geschreven hebt zouden de profielgegevens die door de useProfile hook teruggegeven worden automatisch bijgewerkt moeten worden zodra het inloggen of registreren succesvol afgewerkt is.
Gebruikt de useProfile hook tenslotte om volgende dingen toe te voegen:
- Pagina's die niet bezocht mogen worden door een niet geauthenticeerde gebruiker
- /chat --> Redirect naar login
- /groups --> Redirect naar login
- /login --> Redirect naar login
- /user --> Redirect naar login
- Pagina's die enkel bezocht mogen worden door een niet geauthenticeerde gebruiker
- /login --> Redirect naar groups
Oefening 4: Navigatie deel 2
Nu een gebruiker kan inloggen en we de useProfile hook geschreven hebben, kunnen we de side-nav aanpassen zodat deze enkel de relevante links toont. Als er geen gebruiker ingelogd is, wordt enkel de links naar de home en login pagina's getoond.

Als er een gebruiker ingelogd is, wordt de navigatiebalk aangepast zodat alle links, behalve de link naar de login pagina zichtbaar zijn. Daarnaast wordt de gebruikersnaam van de ingelogde gebruiker getoond in de plaats van de hardcoded tekst 'username' die hiervoor gebruikt werd.

Oefening 5: Profielpagina
Bouw de profielpagina (/user) uit onderstaand screenshot na.

De formulierelementen moeten natuurlijk geïnitialiseerd worden met de waarden die in het profiel staat, als er nog geen voornaam of naam in het profiel staat, wordt een placeholder gebruikt. De submit button is opnieuw de FormSubmitButtonWithLoading component. Om de avatar te tonen gebruik maken van onderstaande code.
const Avatar = styled.img`
vertical-align: middle;
width: 50px;
height: 50px;
border-radius: 50%;
`
const avatarForm = (
<Form.Group controlId="formFile" className="mb-3">
<Form.Label><Avatar/></Form.Label>
<Form.Control type="file" className="d-none"/>
</Form.Group>
)
Oefening 5.1: Profiel bewerken
Schrijf een nieuwe mutation die met behulp van de upsertProfile functie het profiel van de gebruiker aanpast. Natuurlijk moet de ingeladen profieldata weer geïnvalideerd worden na een succesvolle mutatie.
Om de avatar aan te passen moet de gebruiker op bestaande avatar klikken. Vervolgens wordt er een file-picker geopend door de browser en kan de gebruiker een afbeelding selecteren. Het onChange event van een <input type="file"> bevat een property evt.target.files, dit is een array van alle geselecteerde bestanden. Aangezien we maar één bestand laten selecteren er in deze array maximaal één element. Dit element is van het type File, je moet dit File object doorgeven aan de upsertProfile methode (via de mutation), vervolgens zal deze methode het uploaden naar Supabase afhandelen. Om een URL van de afbeelding op te halen kan je gebruik maken van onderstaande methode.
const fileURL = URL.createObjectURL(file)
Gebruik tenslotte ook opnieuw de ResponseMessage component om aan te geven of de aanpassingen succesvol verwerkt zijn.
Oefening 5.2: Uitloggen
Schrijf tenslotte een nieuwe mutation waarmee de gebruiker kan uitloggen.
Oefening 6: Overzicht groepen
Een gebruiker kan een groep aanmaken en zich aansluiten bij een bestaande groep. Een groep kan privé gemaakt worden zodat je zaken kan uittesten zonder dat de code van andere studenten hier invloed op heeft. We beginnen met een overzicht te bouwen van alle groepen en gaan vervolgens verder met het aanmaken van een nieuwe groep en tenslotte met het openen van de detailpagina.
De groep pagina bestaat uit een Bootstrap Tab component, met 4 tabbladen. Zorg dat deze component geladen wordt als het pad /groups bezocht wordt.
Oefening 6.1: My feed
Het eerste tabblad zal gebruikt kunnen worden om een overzicht te krijgen van de meest recente posts in alle groepen waar de gebruiker op geabonneerd is. Voorlopig bevat dit tabblad enkel onderstaande tekst.
Omring de volledige pagina in onderstaande StyledRow component, deze component pas de scrollbar aan. Gebruik de FixedCol component om de kolom met tabbladen te maken, zo blijft deze altijd bovenaan staan, ook als er meer inhoud is dan dat er op het scherm passen.
const StyledRow = styled(Row)`
overflow-y: auto;
overflow-x: hidden;
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #447152;
}
::-webkit-scrollbar-thumb:hover {
background: #549159;
}
`
const FixedCol = styled(Col)`
position: sticky;
right: 0;
top: 0;
height: 100vh;
`

Oefening 6.2: Group Item
Schrijf een nieuwe component GroupItem, deze component krijgt een groep binnen en toont hiervoor de basisinformatie (titel, eigenaar, beschrijving en link naar een detailpagina). Onderstaande code geeft je een idee van de structuur van een groep. De component is gebouwd met de Bootstrap Card component.
{
"id": "0ce34360-0971-4b1d-9eb9-864508540ebb",
"name": "Lorem 1",
"owner": {
"username": "Sebastiaan Henau"
},
"isPrivate": false,
"description": "Lorem ipsum dolor sit amet, ..."
}

Oefening 6.3: My Groups
Het tweede tabblad zal een overzicht geven van de groepen waar de gebruiker zich op geabonneerd heeft, aangezien je momenteel nog op geen enkele groep ingeschreven bent, wordt hier momenteel nog niets getoond. De titel 'My Groups' wordt altijd getoond, ook als de data nog niet opgehaald is. Terwijl de data opgehaald wordt, wordt de LoadingPart component getoond. Tenslotte wordt er gepaste boodschap getoond als de gebruiker nog van geen enkele groep lid is. Schrijf alvast een mutation die de fetchAllSubscribedGroups functie gebruikt om de groepen op te halen en voorzie de code voor het uitprinten van de groepen al.
Oefening 6.4: Explore
Het derde tabblad toont een overzicht van alle publieke groepen en biedt de optie om deze te filteren. De database bevat minstens vier publieke groepen, Lorem 1, Lorem 2, Lorem 3 en Lorem 4. Gebruik de fetchAllPublicGroups methode om een nieuwe mutation te schrijven waarmee de publieke groepen opgehaald worden. Maak gebruik van de useIsEditing hook uit les 4 om er voor te zorgen dat de groepen pas opgehaald worden als de gebruiker niet meer aan het typen is.
Oefening 6.5: Groepen aanmaken
Bouw onderstaand formulier na en schrijft een mutation die gebruik maakt van de createGroup functie en gebruik deze mutation hook om het formulier te implementeren. Vergeet niet om de nodige queries te invalideren.
Als de group succesvol aangemaakt is, wordt de gebruiker na 1.5 seconden geredirect naar de detailpagina. Als alles correct werkt, moet de nieuwe groep ook verschijnen in het my groups tabblad. Als de groep publiek is moet deze ook verschijnen in het explore tabblad.
Oefening 7: Groep detail pagina
De detailpagina voor een groep kan gebruikt worden om de posts en de leden van een groep te bekijken, daarnaast kan een gebruiker zich hier ook in- of uitschrijven in de groep (als deze publiek is). De eigenaar van de groep kan de leden van de groep aanpassen.
Oefening 7.1: Basisinformatie
De basis voor de detailpagina ziet er als volgt uit. In onderstaand screenshot is voor een kolomgrootte van 8 gebruikt voor de posts-kolom en een grootte van 4 voor de leden-kolom. De knop om terug te gaan naar de vorige pagina is gebouwd met het bootstrap icoon bi-chevron-left.
Maak opnieuw gebruik van de StyledRow component die je gebruikt hebt in oefening 6.1.

Oefening 7.2: Terug navigeren naar de Groups pagina
Als de gebruiker op de vorige knop drukt, wordt hij/zij terug naar het tabblad gebracht dat al geselecteerd was. Maak gebruik van context om dit te implementeren.
Oefening 7.3: Groep leden (Overzicht)
De leden-kolom heeft een andere view afhankelijk van wie dit bekijkt. Als de ingelogde gebruiker de eigenaar is van de groep is, wordt er een knop getoond met de tekst "Add or delete group members". Deze knop brengt de gebruiker naar een nieuwe pagina. Daarnaast wordt er voor elke gebruiker een overzicht getoond van iedereen die ingeschreven is in de groep. De eigenaar van de group krijgt een de achtergrondkleur primary, alle andere leden krijgen de achtergrondkleur secondary.
Elke gebruiker die geen eigenaar is van de groep krijgt de optie om zich in of uit te schrijven uit de group.
Oefening 7.4: Groep leden (Admin)
Als de eigenaar van de groep op de "Add or delete group members" knop drukt, wordt hij/zij geredirect naar een detailpagina waar een overzicht van alle gebruikers getoond wordt. De lijst kan gefilterd worden op username. Maak opnieuw gebruik van de useIdEditing hook.
Voor deze oefening is het nuttig om te weten dat de useParams hook gebruikt kan worden om parameters uit te lezen in geneste routes. Dus in een route /groups/:groupId/members kan de groupId parameter ook uitgelezen worden.
Oefening 7.5: Post aanmaken
Om een nieuwe post aan te maken wordt gebruik gemaakt van de React Bootstrap Modal component gebruikt. Zodra de gebruiker op de "New Post" knop drukt, wordt een modaal venster met 2 formulierelementen getoond. Het eerste veld wordt gebruikt om de titel van de nieuwe post in te geven, het tweede veld wordt gebruikt om de inhoud van de post in te geven. Het eerste inputveld wordt automatisch gefocust. De create knop is pas actief als zowel de titel als de inhoud ingegeven zijn.
Maak gebruik van optimistic updates.

Oefening 7.5.1: Post lay-out
Maak gebruik van onderstaande JSX-code om een post weer te geven. Om de publicatiedatum op te generen kan je de string die je terugkrijgt uit de database meegeven aan de constructor van de Data klasse.
const deleteIcon = <Card.Title><BootstrapIcon iconName="trash-fill"/></Card.Title>
const postJSX = (
<Card bg="secondary" text="light" className="p-2 my-2">
<Card.Body>
<div className="d-flex">
<div className="align-self-center me-3">
{/* AVATAR */}
</div>
<div className="flex-grow-1">
<div className="flex-grow-1">
<h4>{/* POST TITLE */}</h4>
<h6>By {/* USERNAME */} at {/* PUBLICATION DATE */}</h6>
</div>
</div>
<div className="align-self-start">
{deleteIcon}
</div>
</div>
<div className="d-grid">
<p className="text-truncate">{/* POST CONTENT */}</p>
</div>
</Card.Body>
</Card>
)
Een post ziet er als volgt uit. De delete knop van de post mag enkel zichtbaar zijn voor de eigenaar van de post. Gebruik ook voor de delete functionaliteit een optimistische update. Als er op de post gedrukt wordt, wordt je naar een detailpagina gebracht. Je kan gebruik maken van de event.stopPropagation() functie om ervoor te zorgen dat de de detailpagina niet geopend wordt als de gebruiker op het delete icoon drukt.

Onderstaande video toont het aanmaken van de posts. Merk op dat de avatar en het tijdstip ook in de optimistische update zitten.
Oefening 8: Post detail
Bouw de detailpagina voor een specifieke post uit volgens onderstaand screenshot.

Oefening 8.1: Commentaar aanmaken
Bouw de functionaliteit uit om een commentaar toe te voegen. Maak opnieuw gebruik van optimistische updates. Een commentaar lijkt heel sterk op een post, pas de JSX-code van deze laatste component aan zodat je het onderstaande bereikt. De reply, load comments en delete knoppen moeten nog niets doen.
Oefening 8.2: Commentaar verwijderen
De delete knop is enkel zichtbaar als de eigenaar van de commentaar de pagina bezoekt. Natuurlijk werk je hier weer met optimistische updates.
Oefening 8.3: Commentaar op een commentaar
De gebruiker heeft de optie om een commentaar te plaatsen op een bestaande commentaar. Als een gebruiker op de reply knop drukt wordt een formulier getoond waarmee de nieuwe commentaar ingegeven kan worden. Als de gebruiker de commentaar aanmaakt wordt dit via dezelfde mutation als voor het aanmaken van een gewone level-1 commentaar gedaan. Let op, de optimistische update zal niet noodzakelijk werken. Controleer eerst of een post in de cache zit voor je een optimistische update probeert uit te voeren.

Oefening 8.4: Sub-commentaren laden
Als de gebruiker op de load comments knop drukt worden de eventuele sub-commentaren geladen. De knop verdwijnt nadat de gebruiker hierop gedrukt heeft. Om de commentaren te laden wordt gebruik gemaakt van een nieuwe component CommentLoader die omringt wordt door een Suspense component en de LoadingPart component toont terwijl de data aan het laden is. De CommentLoader component toont alle sub-commentaren opnieuw via de IComment component die hierboven gebruikt is.
Zorg er voor dat na een optimistische update de kind-commentaren sowieso ingeladen worden.