3. Bouwen - IvarSchuyt/Sprint-20---Individueel---YourJourney GitHub Wiki

Atomic design

Atomic design

Werkwijze atomic design

Ik heb ervoor gekozen om dit project met atomic design te werken. Atomic design en SvelteKit gaan heel goed samen en er is een specifieke manier van routing om dit te laten werken.Deze routing heb ik voor al mijn components ook aangehouden.

Mijn componenten deel ik op in verschillende mappen in de $lib: atoms, molecules en organisms. Vervolgens haal ik de componenten allemaal op dezelfde plek op. Dit doe ik als volgt in de $lib/ index.js

// Hier export je alle atoms --------------------------------------------------------------------------------------------------------------
export { default as Logo } from "./atoms/logo.svelte";
// Hier export je alle molecules ----------------------------------------------------------------------------------------------------------
export { default as Nav } from "./molecules/nav.svelte";
// Hier export je alle organisms ----------------------------------------------------------------------------------------------------------
export { default as Header } from "./organisms/header.svelte";

Als ik vervolgens een of meerdere componenten wil gebruiken, bijvoorbeeld op de homepage (+page.svelte), haal ik ze weer op uit deze verzamelplaats($lib/index.js):

<script>
    	import { Hero, Text, Program, Goals, Offer } from '$lib/index.js';
</script>

Hygraph

Hygraph

Keuze CMS

In de eerste instantie zou ik bij dit project Wordpress als CMS gebruiken. Na meerdere keren advies gevraagd te hebben aan een docent met veel ervaring met Wordpress als CMS, heb ik besloten om toch niet voor Wordpress te kiezen. Er zaten te veel haken en ogen aan, waardoor ik veel minder tijd zou hebben om mezelf uit te dagen om een daadwerkelijk mooi en kwalitatief product te maken. Ik wou dit toch liever dan het gebruiken van Wordpress. Na meerdere keren peilen met de opdrachtgever heb ik de knoop doorgehakt om toch Hygraph te gaan gebruiken als CMS.

Ik heb voor Hygraph gekozen, omdat ik al redelijk bekend was met deze CMS en ik door de beperkte hoeveelheid tijd liever niet tegen al te veel problemen met mijn CMS aan wou lopen. Alhoewel ik mezelf niet per se uit wou dagen met Hygraph, viel me wel meteen iets op wat ik nog niet eerder had gebruikt: Hygraph components. Omdat ik al met atomic design wou werken leek dit perfect om uit te proberen, zodat ik zelfs in mijn CMS atomic design kon gebruiken.

Hygraph components

Doormiddel van components in Hygraph heb ik dus perfect mijn workflow met SvelteKit kunnen nabootsen, waardoor de koppeling tussen CMS en code goed overzichtelijk bleef. In de volgende afbeeldingen zie je hoe ik een component indeel en hoe ik vervolgens een pagina met meerdere componenten indeel:

Screenshot 2024-05-11 at 15 28 10

Screenshot 2024-05-11 at 15 28 26

Data ophalen

Ik gebruik als voorbeeld de volgende query om data voor de header op te halen:

import { gql } from "graphql-request";
import { hygraph } from "$lib/utils/hygraph.js";

export async function load() {
  let query = gql`
    query MyQuery {
      pages {
        header {
          home
          program
          community
          extras
          account
          logo {
            logo {
              url
            }
          }
        }
      }
    }
  `;

  const data = await hygraph.request(query);
  return data;
}

Een fijn bijproduct van het gebruik van components in Hygraph is het feit dat de content overzichtelijk onder de bijbehorende component staat.

Ik kies er nu voor om al mijn data op te halen via +layout.server.js, omdat dit volgens de documentatie aan te roepen is door elke route die je aanmaakt. Dit bespaart me nu tijd, omdat ik geen aparte queries hoef te doen per pagina. Op de lange termijn denk ik dat het beter is voor de performance om alleen data op te halen via +layout.server.js als ik die data op meerdere pagina's gebruik en vervolgens de data voor individuele pagina's via de desbetreffende +page.server.js op te halen. Ik weet niet of het opsplitsen van de query uitmaakt qua performance als ik de site server side laat renderen, dus hier moet ik me iets meer in gaan verdiepen.

Data inladen

Data binnen components inladen doe ik als volgt:

<script>
    import { NavIcon, Profile } from '$lib/index.js';
    export let data;

    const hygraphData = data.pages[0];

</script>
 
<nav>
     <details open>
        <summary>
            <NavIcon />
        </summary>
        <ul>
            <li><a href="/">{hygraphData.header.home}</a></li>
            <li><a href="/{hygraphData.header.program}">{hygraphData.header.program}</a></li>
            <li><a href="/{hygraphData.header.community}">{hygraphData.header.community}</a></li>
            <li><a href="https://www.yourjourney.academy/extras/" target="_blank">{hygraphData.header.extras}</a></li>
            <li><a href="/{hygraphData.header.account}">{hygraphData.header.account}</a></li>
            <li><a href="/{hygraphData.header.account}" aria-label="Mijn Account"><Profile /></a></li>
        </ul>
    </details>
</nav>

Vervolgens laad ik op de pagina alle content voor deze component in:

<script>
	import { page } from '$app/stores';
	import { Nav, Footer } from '$lib/index.js'

	export let data;
</script>

<Nav {data} />

<slot />

<Footer {data} />

Eigenlijk moet ik om elk stukje data op te halen de volgende regel schrijven: data.pages[0].header.mijncontent. Om dit wat overzichtelijker te maken heb ik data.pages[0] vervangen door hygraphData door de volgende regel code: const hygraphData = data.pages[0];.

Responsive images

Responsive images

Responsive img

Ik heb op advies van Krijn gebruik gemaakt van responsive images om de performance van de site te verbeteren. Om mezelf niet helemaal gek te maken van deze hoeveelheid afbeeldingen heb ik ervoor gekozen om voor grote afbeeldingen maximaal drie formaten in te laten laden. Het type afbeelding waar ik voor gekozen heb is WebP. In mijn geval komt 'responsive images' neer op het feit dat ik voor smallere schermen, kleinere foto's inlaad zodat er een minder groot bestand ingeladen hoeft te worden.

Om dit werkend te krijgen heb ik eerst bij de documentatie hierover gekeken en vervolgens naar Krijn zijn website die hij als voorbeeld gebruikte.

Op Hygraph laad ik het als volgt in: (het cijfer achter de 'hero' staat voor het percentage grootte ten opzichte van de originele afbeelding) Screenshot 2024-05-11 at 15 38 26

Op mijn homepage ziet het er als volgt uit:

        <picture>
            <source media="(min-width: 900px)" srcset="{img100}">
            <source media="(min-width: 600px)" srcset="{img50}">
            <img src="{img25}" alt="">
        </picture>

Responsive background-image

Ik kwam er tijdens het testen achter dat mijn background-image een lange laadtijd had op mobiel. Het eerste wat ik probeerde was het omzetten van png naar webp te converteren, maar dit zorgde er op een of andere manier voor dat de laadtijd nog langer werd. Ik wou graag een background-image blijven gebruiken in plaats van een img element. Na wat zoeken op internet heb ik deze bron over image-set gevonden. Dit is een soort srcset maar dan voor de CSS background-image. Je kan hier jammer genoeg niet een width aan meegeven, maar wel '1x' of '2x' waarmee je de pixeldichtheid van het display kan aangeven. De foto bij 1x is dus de foto met een lage kwaliteit en de foto met 2x heeft een betere kwaliteit.
Dit is hoe het er in mijn code uitziet:

<section 
style="background-image: image-set( url('{data.pages[0].hero.fingerprints1.url}') 1x,
url('{data.pages[0].hero.fingerprints2.url}') 2x);">

PE Form + Post naar CMS

Responsive images

PE Form

<script>
    import { enhance } from '$app/forms'
    import { Button } from '$lib/index.js'
    export let form
    export let data;

    let loading = false

    // Custom enhancement function
    function handleForm(){
        loading = true

        return async ({ result, update }) => {
            // fake 400ms delay voor user feedback
            await setTimeout(() => {
                update()

                loading = false  
            }, 400);
        }
    }
</script>

<section>

    <h1>Ervaringen</h1>
    <h2>Wat vonden anderen van onze service?</h2>
    <p class="p-top">Wij vinden het belangrijk dat wij iedereen zo goed, persoonlijk en efficiënt mogelijk helpen. Lees hieronder hoe andere studenten onze service hebben ervaren!</p>

    <div>
        <!-- Reviews inladen -->
        {#each data.communities as community}
            <article>
                <span class="name">{community.name}</span>
                <p class="ervaring">{community.ervaring}</p>
            </article>
        {/each}
    </div>

    <!-- Enhanced form -->
    <form action="/Ervaringen" method="POST" use:enhance={handleForm}> 
        <h3>Deel jouw ervaring</h3>
        
        {#if form?.error}
            <p class="message fail">{form.message}</p>
        {/if}

        <fieldset>
            <!-- ?? '' means "if form?.name is null or undefined, use the empty string ('')" -->
            <label><span>Naam</span> <input type="text" name="name" minlength="2" required value="{form?.name ?? ''}" placeholder="Jan Jansen"/></label>
        </fieldset>

        <fieldset>
            <label class="label-ervaring"><span>Voer hier je ervaring in</span> <textarea name="ervaring" rows="10" required value="{form?.ervaring ?? ''}"></textarea></label>
        </fieldset>

            <Button buttonText="Versturen" />
            {#if loading }
                <!-- Aninamtie voor custom enhancement -->
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 150"><path fill="none" stroke="#3d4666" stroke-width="16" stroke-linecap="round" stroke-dasharray="300 385" stroke-dashoffset="0" d="M275 75c0 31-27 50-50 50-58 0-92-100-150-100-28 0-50 22-50 50s23 50 50 50c58 0 92-100 150-100 24 0 50 19 50 50Z"><animate attributeName="stroke-dashoffset" calcMode="spline" dur="2" values="685;-685" keySplines="0 0 1 1" repeatCount="indefinite"></animate></path></svg>
            {/if}
            
            <!-- Melding voor user feedback -->
            {#if form?.success}
                <p class:active={form?.success} id="feedback">Bedankt voor het delen van jouw ervaring!</p>
            {/if}
    </form>

</section>

Post

Ik heb bij het maken van de postfunctie een hoop gehad aan de documentatie van Hygraph en Github copilot. Ik had vaak het gevoel dat de documentatie een beetje incompleet en dat simpele stappen zoals API autorisatie regelen nergens stond. Gelukkig had Github copilot altijd de oplossing voor me.

import { fail } from "@sveltejs/kit";
import { request as graphqlRequest } from "graphql-request";
import { gql } from "graphql-request";
import { hygraph } from "$lib/utils/hygraph.js";

export const prerender = false;

// Data naar Hygraph sturen
export const actions = {
  default: async ({ request, url }) => {
    const formData = await request.formData();
    let name = formData.get("name");
    let ervaring = formData.get("ervaring");

    // Zorg dat je een enter kan zetten in de textarea
    name = name.replace(/\r?\n/g, "\\n");
    ervaring = ervaring.replace(/\r?\n/g, "\\n");

    // Check of de naam minimaal 2 karakters bevat
    if (name.length < 2)
      return fail(400, {
        error: true,
        message: "Naam moet minstens 2 karaters bevatten",
        name,
        ervaring,
      });

    // Maak nieuwe content aan voor Hygraph
    const mutation = `
      mutation {
        createCommunity(data: { name: "${name}", ervaring: "${ervaring}" }) {
          id
          name
          ervaring
        }
      }
    `;

    // Hygraph url
    const endpoint =
      "https://api-eu-central-1-shared-euc1-02.hygraph.com/v2/clvv6cpib12hf07utq7pabixw/master";

    // Hygraph autorisatie
    const HYGRAPH_TOKEN = import.meta.env.VITE_HYGRAPH_KEY;
    const headers = {
      Authorization: `Bearer ${HYGRAPH_TOKEN}`,
    };

    // Voer de mutatie uit
    const postData = await graphqlRequest(
      endpoint,
      mutation,
      undefined,
      headers
    );
    return { success: true, postData };
  },
};
// Haal de data op uit Hygraph
export async function load() {
  let query = gql`
    query MyQuery {
      communities {
        name
        ervaring
      }
    }
  `;
  const data = await hygraph.request(query);
  return data;
}

Uitleg aan oprachtgever

Mijn opdrachtgever heeft niet zo veel kennis van backend en javascript. Om het proces van deze post naar Hygraph in beeld te brengen heb ik een sketchnote gemaakt. Ik heb hier de post vergeleken met de echte post om het voorbeeld wat duidelijker te maken. Na de uitleg snapte de opdrachtgever het proces helemaal!

Screenshot 2024-05-31 at 10 31 26

View Transitions API

View Transitions API

View Transitions

Ik heb aan de hand van de hand van een bron over deze API zelf en een bron over de API in combinatie met SvelteKit view transitions tussen mijn pagina's gemaakt. Ik ben achteraf nog steeds verbaasd over hoe weinig code hier voor nodig is.

Ik heb de volgende code in mijn +layout.svelte gezet:

    import { onNavigate } from '$app/navigation';

    onNavigate(function(navigation) {
        if (!document.startViewTransition) return;
        return new Promise(function(resolve) {
            document.startViewTransition(function() {
                resolve();
                navigation.complete;
            });
        });
    });

Dit werkte al helemaal perfect, maar in de documentatie las ik dat je heel makkelijk rekening kan houden met gebruikers die 'reduced motion' aan hebben staan. Om de toegankelijkheid voor die mensen te verbeteren heb ik de volgende code in mijn global.css gezet:

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

GSAP

GSAP

Screen Recording 2024-05-30 at 10 58 41

Ik heb bij dit project voor het eerst GSAP gebruikt. Als eerste keek ik naar het voorbeeld wat Cyd ons in de les heeft laten zien als oefening.

In de code heb eerst npm install gsap in de terminal gezet. Dit installeerde de GSAP library in mijn project. Vervolgens heb ik op de juiste pagina GSAP en Scrolltrigger geïmporteerd: import {gsap} from "gsap/dist/gsap"; import {ScrollTrigger} from "gsap/dist/ScrollTrigger";

Vervolgens heb ik in de onMount de elementen gedefinieerd die ik gebruik tijdens de animatie:

    onMount (() => {
        gsap.registerPlugin(ScrollTrigger);
        const scrolltrigger = document.querySelector("section");
        if (scrolltrigger) {
        const animation = scrolltrigger.querySelectorAll("circle, line, .animation");

Daarna maak ik de animatie aan, kies ik een start- en eindpunt, en zet ik scrub (de animatie gebeurt 'een voor een') aan:

  gsap.to(animation, {
            scrollTrigger: {
			// gebruik de container als relatieve trigger voor de start en end van de animatie
			trigger: scrolltrigger,
			scroller: "body",
			// eerste value is relatief aan het trigger-element, tweede value is relatief aan de 'scroller'
			start: "top +100",
			end: "center center",
			scrub: true
		},

Als laatste pas ik de styling doe. Dit zorgt voor het effect dat de bolletjes tevoorschijn komen. Uiteindelijk is dit de volledige code:

 onMount (() => {
        gsap.registerPlugin(ScrollTrigger);
        const scrolltrigger = document.querySelector("section");
        if (scrolltrigger) {
        const animation = scrolltrigger.querySelectorAll("circle, line, .animation");
        gsap.to(animation, {
            scrollTrigger: {
			// gebruik de container als relatieve trigger voor de start en end van de animatie
			trigger: scrolltrigger,
			scroller: "body",
			// eerste value is relatief aan het trigger-element, tweede value is relatief aan de 'scroller'
			start: "top +100",
			end: "center center",
			scrub: true
		},
		stagger: 0.1,
        stroke: "green",
        opacity: "1",
	});
}
        }
    );

De animatie vind plaats op verschillende SVG's die ik zelf heb gemaakt. De HTML ziet er zo uit:

<svg>
        <circle r="15" cx="50" cy="125" fill="green"/>
        <circle r="25 " cx="50" cy="125" fill="transparent" stroke="green" stroke-width="1"/>
        <line x1="50" y1="250" x2="50" y2="150" stroke="black"/>
        <circle r="15" cx="50" cy="275" fill="green" class="animation"/>
        <circle r="25 " cx="50" cy="275" fill="transparent" stroke="black" stroke-width="1" />
        <line x1="50" y1="425" x2="50" y2="300" stroke="black"/>
        <circle r="15" cx="50" cy="450" fill="green" class="animation"/>
        <circle r="25 " cx="50" cy="450" fill="transparent" stroke="black" stroke-width="1" />
        <line x1="50" y1="575" x2="50" y2="475" stroke="black"/>
        <circle r="15" cx="50" cy="600" fill="green" class="animation"/>
        <circle r="25 " cx="50" cy="600" fill="transparent" stroke="black" stroke-width="1" />
        <line x1="50" y1="725" x2="50" y2="625" stroke="black"/>
        <circle r="15" cx="50" cy="750" fill="green" class="animation"/>
        <circle r="25 " cx="50" cy="750" fill="transparent" stroke="black" stroke-width="1" />
        <line x1="50" y1="875" x2="50" y2="775" stroke="black"/>
        <circle r="15" cx="50" cy="900" fill="green" class="animation"/>
        <circle r="25 " cx="50" cy="900" fill="transparent" stroke="black" stroke-width="1" />
        <line x1="50" y1="1050" x2="50" y2="925" stroke="black"/>
        <circle r="15" cx="50" cy="1075" fill="green" class="animation"/>
        <circle r="25 " cx="50" cy="1075" fill="transparent" stroke="black" stroke-width="1" />
        <line x1="50" y1="1200" x2="50" y2="1100" stroke="black"/>
        <circle r="15" cx="50" cy="1225" fill="green" class="animation"/>
        <circle r="25 " cx="50" cy="1225" fill="transparent" stroke="black" stroke-width="1" />
        <line x1="50" y1="1355" x2="50" y2="1250" stroke="black"/>
        <circle r="15" cx="50" cy="1380" fill="green" class="animation"/>
        <circle r="25 " cx="50" cy="1380" fill="transparent" stroke="black" stroke-width="1" />
    </svg>

Zonder JavaScript werkt de animatie dus niet, omdat het client-side is. De animatie blijft dat altijd in de startpositie staan. In het geval van deze animatie is dat niet hinderlijk en is het dus een prima PE toepassing van GSAP.

GSAP aangeraden aan opdrachtgever

Screenshot 2024-05-31 at 10 40 20 Screenshot 2024-05-31 at 10 39 20

Prefer reduced motion

Aan het einde van het project heb ik deze animatie toegankelijker gemaakt, door hem uit te zetten wanneer iemand 'reduce motion' aan heeft staan. Ik heb het op de volgende manier toegepast:

    onMount (() => {
        gsap.registerPlugin(ScrollTrigger);
        const scrolltrigger = document.querySelector("section");
        const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

        if (scrolltrigger && !prefersReducedMotion) {
            const animation = scrolltrigger.querySelectorAll("circle, line, .animation");
            gsap.to(animation, {
                scrollTrigger: {
                    trigger: scrolltrigger,
                    scroller: "body",
                    start: "top +100",
                    end: "center center",
                    scrub: true
                },
                stagger: 1,
                stroke: "green",
                opacity: "1",
            });
        }
    });

Accessible links

Accessible links

Accessible links

Soms gebruik ik een link met daarin alleen een svg/icoon als inhoud. Een persoon met screenreader kan daar nooit uit afleiden wat de link precies inhoud. Om dit duidelijker te maken heb ik gebruik gemaakt van aria-label. Door dit toe te voegen kan ik extra informatie geven zodat de gebruiker weet waar de link hem/haar naartoe stuurt.

<a href="/{hygraphData.header.account}" aria-label="Mijn Account"><Profile /></a>

Github Copilot

Github Copilot

Github Copilot

Tijdens het werken aan dit project heb ik veel gewerkt met Github Copilot. Het was de eerste keer dat ik dit gebruikte, maar ik gebruikte het al snel dagelijks. Het heeft me enorm geholpen met specifieke vragen over bepaalde CSS onderwerpen, maar ook bijvoorbeeld met het maken van een post functie naar Hygraph. Vaak moest ik me wel eerst inlezen met een bron om gerichte vragen te stellen, maar wanneer ik dit eenmaal gedaan had kon ik meestal snel aan de slag met mijn nieuwe componenten. Naast dit kan het je ook goed helpen met code aanvullen. Het voorspelt vaak wat je wilt typen en op sommige momenten komt dat super goed uit. Ook kan je geholpen worden wanneer je simpelweg gewoon lui bent. Ik vraag weleens of mijn code meer DRY kan worden gemaakt. Soms zie ik al dat ik bepaalde delen CSS samen kan voegen, maar is het toch sneller om Copilot dit te laten doen. Ik vervang wel altijd stukje voor stukje de bestaande code door de aangeleverde code. Op deze manier, wanneer er iets toch breekt, kan je meteen zien door welk deel van de code dit gebeurt. Dan moet je toch zelf een beetje nadenken over hoe je dit op gaat lossen.

Een voorbeeld van hulp vragen en antwoord:
Screenshot 2024-06-03 at 22 16 46

⚠️ **GitHub.com Fallback** ⚠️