Herhalingsoefening
Herhalingsoefening
Tijdens deze oefeningenreeks vertrek je van een v0.dev project, en bouw je stap voor stap een project dat iets kleiner is dan het project voor de eindopdracht.
De belangrijke punten voor het project worden in deze oefening herhaald, daarnaast wordt ook besproken hoe je afbeeldingen kunt toevoegen aan je project zonder hier een externe service voor te gebruiken.
Startbestanden
Download onderstaande startbestanden (dezelfde als voor het project) en voeg deze samen met de code die door v0.dev gegenereerd is.
Startbestanden (v0.dev)
Startbestanden (project)
Authenticatie
De v0 startbestanden bevatten pagina's voor login en registratie, maar deze worden nog niet getoond. Pas de code aan zodat de gebruiker ingelogd moet zijn voordat de app gebruikt kan worden, een niet ingelogde gebruiker wordt automatisch geredirect naar de login pagina.
Een ingelogde gebruiker kan de login en register pagina's niet bezoeken, in dat geval wordt de gebruiker automatisch geredirect naar de dashboardpagina.
Implementeer tenslotte het inlog en registratieproces via de voorziene code in de startbestanden. Vergeet niet om de logout knop in de navbar te implementeren.
Prisma schema
In @/types/index.ts zijn verschillende TypeScript interfaces gebouwd die doorheen de oefening gebruikt worden. Gebruik deze om een Prisma schema te bouwen en een migratie aan te maken. Voeg naast de interfaces nog een Admin rol toe en een één-op-veel relatie tussen de User en ReadHistory tabellen.
Verwijder de interfaces vervolgens en vervang deze door de gegenereerde Prisma types.
Op elk van de pagina's, in de V0 code, is een set met mock data te vinden, verhuis deze naar het seed script. Zorg er tenslotte ook voor dat er twee gebruikers aangemaakt worden, één voor elke rol. Elk van deze gebruikers moet een voor elk boek een leesgeschiedenis hebben.
Afbeeldingen
Voor de boeken en auteurs moet er een afbeelding bewaard worden, de V0 code gebruik hier een placeholder voor die voor elk boek en elke auteur hetzelfde is.
Zoek online (of in de oplossingen onderaan deze pagina) een cover voor elk van de boeken en een profielfoto voor elk van de auteurs. Plaats deze vervolgens in de 'prisma/seedImages' map. Pas het seed script vervolgens aan zodat de afbeeldingen voor elk boek geüpload worden naar de public map van de applicatie.
Notitie
Dit is waarschijnlijk niet wat je in een productie app wilt doen, in dat geval maak je best gebruik van een externe storage bucket zoals beschreven in appendix 7.
Voor een schoolproject is dit echter een prima oplossing.
Hiervoor doorloop je twee stappen:
- Schrijf een algemene functie die zowel door het seed script als door de Next applicatie gebruikt kan worden om een afbeelding te uploaden naar de public map. Deze functie neemt File object als input, vormt deze om naar een ArrayBuffer en schrijft de buffer vervolgens weg naar de public/images map onder een unieke bestandsnaam (uuid). Tenslotte geeft deze functie de publieke URL van de afbeelding terug (die in de database bewaard wordt).
- Schrijf een functie specifiek in het seed script die de seedimages uitleest als buffer en converteert naar een File object. Deze functie roept vervolgens de algemene upload functie aan om de afbeelding te uploaden.
Uitgewerkte upload functies
import {mkdir, writeFile, readFile} from 'node:fs/promises'
import {randomUUID} from 'node:crypto'
import {existsSync} from 'node:fs'
export async function uploadSeedFile(name: string): Promise<string> {
const fileContents = await readFile(`./prisma/seedImages/${name}`)
const file = new File([fileContents.buffer], 'name')
return uploadFile(file)
}
const fileRoot = '/images'
export async function uploadFile(file: File): Promise<string> {
if (!existsSync(`./public/${fileRoot}`)) {
await mkdir(`./public/${fileRoot}`)
}
const buffer = await file.arrayBuffer()
const path = `${fileRoot}/${randomUUID()}.${file.type.split('/')[1]}`
await writeFile(`./public${path}`, Buffer.from(buffer))
return path
}Validatieschema's
Schrijf aan de hand van de Prisma modellen Zod validatieschema's voor alle CRUD-operaties op alle modellen.
Zoals hierboven besproken moet de gebruiker een cover voor een boek kunnen uploaden, om deze property te valideren kan je onderstaand Zod schema gebruiken.
import {z} from 'zod'
// Standaard accepteert een server action een body van maximaal 1MB, om bestanden groter dan 1MB te
// kunnen uploaden moet je de body size verhogen in de config.
// https://nextjs.org/docs/app/api-reference/config/next-config-js/serverActions
const fileSizeLimit = 3 * 1024 * 1024 // 3MB
export const imageSchema = z.preprocess(
image => {
// On the client the submitted value will be a FileList, on the server there will just be one File object.
// FileList isn't defined on the server, so we can't use that in the condition.
if (image instanceof Object && Object.keys(image).includes('0')) {
return (image as unknown[])[0]
}
return image
},
z
.instanceof(File)
.refine(file => ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp'].includes(file.type), {
message: 'Invalid image file type, only png, jpeg, jpg, gif and webp images are allowed',
})
.refine(file => file.size <= fileSizeLimit, {
message: 'File size should not exceed 3MB',
}),
)Server functies
Schrijf server functies (en server actions) voor alle CRUD-operaties op alle modellen. Het aanmaken, bewerken en verwijderen van een boek, auteur, serie, attribuut en genre mag enkel door een admin gebruiker gedaan worden.
Het aanmaken en bewerken van een leesgeschiedenis entry mag door elke gebruiker gedaan worden, maar natuurlijk enkel voor zijn/haar eigen account.
User Interface
Pas de UI aan zodat alle CRUD-operaties geïmplementeerd zijn. Aangezien een gewone gebruiker enkel de leesgeschiedenis mag aanmaken en bewerken, zorg je ervoor dat de andere formulier niet getoond worden aan deze gebruikers en dat de knoppen verborgen zijn.
Zorg ervoor dat de data op elke pagina gefilterd kan worden via de searchbars die al aanwezig zijn in de startbestanden. Je doet dit server side, i.e. de searchbars zijn een formulier dat bij een submit de pagina herlaadt met de opgegeven zoekterm als query parameter.
Om de UI te implementeren kan je gebruik maken van volgende componenten die een volledige integratie bieden tussen server functions, Zod en Hook Form. Dat gezegd, raden we iedereen aan om deze componenten zelf te schrijven als oefening.
Hoe volgende punten in het achterhoofd als je een complexere form component bouwt:
- Er moet altijd een hidden form input veld zijn dat gesynchroniseerd is met Hook Form.
- Gebruik de Controller als wrapper rond complexe UI elementen die verder gaan dan een standaard form element.
- Checkboxes en radio buttons worden pas gesubmit als ze ooit aangevinkt zijn, zorg er dus voor dat deze altijd een default waarde hebben. Anders zou de browser deze velden kunnen weglaten uit de submit data.
- <FormTextArea>: Een component zoals de FormInput die in de les besproken is, maar dan voor een textarea element.
- <FormDatePicker>: De date picker component zoals besproken in hoofdstuk 6.
- <FormSelect>: Een select component.
- <FormMultiSelect>: Een multi select component op basis van checkboxes. De opties kunnen gefilterd worden.