3. Bouwen - Trisjan/Freago GitHub Wiki

Wat voor deployement zal er gebruikt worden?

Ik heb gekozen voor Vercel.

Form (Progressively Enhanced)

Ik heb een form gebouwd die progressive enhanced is om zo de belangen van de eindgebruiker te bewaren.

Client side code

Ik heb een simpele HTML form gemaakt op de client side en daar een aantal extra functies aan toegevoegd zodat de gebruiker beter snapt wat er gebeurt in het proces. De volgende code heb ik gebruikt op de client side:

<script>
	import { PrismicRichText } from '@prismicio/svelte';
	import { enhance } from '$app/forms';
	import { page } from '$app/stores';

	/** @type {import("@prismicio/client").Content.ContactFormSlice} */
	export let slice;

	let submitted = false;
	let loading = false;
	let formFeedback = '';

	async function handleSubmit() {
		loading = true;
		submitted = true; // Zet submitted op true zodra het formulier is ingediend

		// Reset submitted en loading na een korte vertraging (bijvoorbeeld 1 seconde)
		setTimeout(() => {
			loading = false;
			submitted = false;
			formFeedback = 'Bedankt! Het formulier is succesvol verzonden.';
		}, 1000);
	}

	function closePopup() {
		formFeedback = ''; // Sluit de pop-up door de feedback te verwijderen
	}
</script>

Boven aan de script import ik een aantal modules zodat ik gebruik kan maken hiervan later in de code. Enhance is een module die aangeboden wordt vanuit sveltkit om zo je form te kunnen optimaliseren.

import { PrismicRichText } from '@prismicio/svelte';
import { enhance } from '$app/forms';
import { page } from '$app/stores';

Daarna export ik slice zodat ik te werk kan gaan met de data vanuit prismic. Dit is het zelfde als export let data; wat je gebruikt wanneer je hygraph gebruikt.

export let slice;

Daarna definieer ik een aantal variabeles wat mij zal helpen bij het progressive enhancen van deze form. Ik zet de variabelen submitted en loading op 'false' als een soort boolean variabeles. De formFeedback variabele is een string die ik leeg laat zodat ik hier later text in kan zetten.

let submitted = false;
let loading = false;
let formFeedback = '';

Daarna maak ik 2 functies aan. Bij de functie van handleSubmit worden de variabeles loading en submitted naar true omgezet. Vervolgens gaat er een een soort van timer in die ervoor zorgt dat loading en submitted terug naar false gaan en formFeedback een gevulde string krijgt met feedback naar de gebruiker toe na het uitvoeren van een taak. Na 1 seconde zal dit gebeuren. Vervolgens heb ik een functie genaamd closePopup en hierin wordt de variabele formFeedback terug gezet naar een lege string. Deze functie zal ik aanroepen nadat de handleSubmit functie is uitgevoerd.

	async function handleSubmit() {
		loading = true;
		submitted = true; // Zet submitted op true zodra het formulier is ingediend

		// Reset submitted en loading na een korte vertraging (bijvoorbeeld 1 seconde)
		setTimeout(() => {
			loading = false;
			submitted = false;
			formFeedback = 'Bedankt! Het formulier is succesvol verzonden.';
		}, 1000);
	}

	function closePopup() {
		formFeedback = ''; // Sluit de pop-up door de feedback te verwijderen
	}

De volgende code is mijn form met wat extra's om deze progressive te enhancen.

<section data-slice-type={slice.slice_type} data-slice-variation={slice.variation}>
	<PrismicRichText field={slice.primary.title} />
	<PrismicRichText field={slice.primary.description} />
	{#if formFeedback}
		<section class="popup">
			<article class="popup-content">
				<p>{formFeedback}</p>
				<button on:click={closePopup}>Sluiten</button>
			</article>
		</section>
	{/if}
	<form method="POST" action="/" on:submit={handleSubmit} use:enhance>
		<input type="hidden" name="access_key" value="4d59ea0f-13b8-4119-b6b8-b5cb5c38e663">
		<section class="group">
			<label for="email">Email</label>
			<input required type="email" id="email" name="email" />
		</section>
		<section class="group">
			<label for="message">Message</label>
			<input required type="text" name="message" id="message" minlength="4" maxlength="500" />
		</section>
		<section class="group">
			<button class:submitted class:loading disabled={submitted}>
				{loading ? 'Loading' : 'Submit'}
			</button>
		</section>
	</form>
</section>

In de volgende code maak ik gebruik van prismic components en zet ik daar de data in die ik mee krijg vanuit het CMS.

<PrismicRichText field={slice.primary.title} />
<PrismicRichText field={slice.primary.description} />

Daarna heb ik een if statement die een pop up laat zien als die true is. Dus wanneer formFeedback geen lege string is, wordt deze popup vertoond met de inhoud van de formFeedback variabele. In deze popup is er een button die de functie closePopup uitvoert wanneer je erop klikt. Deze functie hebben we eerder vormgegeven in onze clientside javascript. Het leegt de formFeedback en dat zorgt ervoor dat de popup verdwijnt omdat de string leeg is.

	{#if formFeedback}
		<section class="popup">
			<article class="popup-content">
				<p>{formFeedback}</p>
				<button on:click={closePopup}>Sluiten</button>
			</article>
		</section>
	{/if}

In de volgende code heb ik een form met de method "post" en een functie die uitgevoerd wordt wanneer de form gesubmit wordt en maak ik gebruik van de "use:enhance" feature van sveltekit. De method post geeft aan dat de form data opstuurt. De on:submit zorgt ervoor dat een functie wordt getriggerd op het moment dat de form wordt ingedient door de eindgebruiker. Daarmee kunnen we dus bepaalde dingen uitvoeren zoals het vertonen van feedback aan de gebruiker, dat is dan ook de reden waarom ik het gebruik in deze instantie. Daarna wordt er aan het einde een "use:enhance" gebruikt om de form te enhancen. Wat de "use:enhance" property doet, is dat het ervoor zorgt dat de formdata opgestuurd wordt zonder dat het een pagina reload nodig heeft en maakt het ook nog eens de form leeg.

	<form method="POST" action="/" on:submit={handleSubmit} use:enhance>
		<input type="hidden" name="access_key" value="4d59ea0f-13b8-4119-b6b8-b5cb5c38e663">
		<section class="group">
			<label for="email">Email</label>
			<input required type="email" id="email" name="email" />
		</section>
		<section class="group">
			<label for="message">Message</label>
			<input required type="text" name="message" id="message" minlength="4" maxlength="500" />
		</section>
		<section class="group">
			<button class:submitted class:loading disabled={submitted}>
				{loading ? 'Loading' : 'Submit'}
			</button>
		</section>
	</form>

Ik maak gebruik van een plugin die ervoor zorgt dat ik de inhoud van forms kan sturen naar mijn eigen mail. Hiervoor heb ik een token nodig om dit mogelijk te maken.

<input type="hidden" name="access_key" value="4d59ea0f-13b8-4119-b6b8-b5cb5c38e663">

Aan het einde van de form heb ik nog een button die wat speciaals doet als javascript aan staat bij de eindgebruiker. Wanneer er op de button gedrukt wordt, worden de classes submitted en loading toegevoegd aan de button element. Deze classes heb ik gestyled in de css. De button heeft ook een property disabled. Dit is een boolean en deze kan true of false zijn. De submitted variabele is ook een boolean en staat standaard op "false". Als de form gesubmit werd, werd er een functie getriggerd waarbij de variabele submitted true werd. Dat betekend dus dat als de form ingediend werd, deze button op dissabled werd gezet zodat de gebruiker niet nogmaals op de button kan drukken. Vervolgens krijgt de button een text op basis van de status van de status van de form. Als de form succesvol is ingediend "$page.form?.success" = true, dan laat je de tekst "thank you" zien. Als deze false is wordt er naar de volgende status gekeken. Als Loading = true, dan laat de tekst "loading" zien. Als beide false zijn, laat dan gewoon de tekst "submit" zien.

<button class:submitted class:loading disabled={submitted}>
	{$page.form?.success ? 'Thank you ✨' : loading ? 'Loading' : 'Submit'}
</button>

Server side code

En voor het afhandelen van de form en opsturen heb ik de volgende code (server side). Het afhandelen van de form op de server zorgt ervoor dat deze request altijd door zal komen (mits de eindgebruiker een redelijke internetverbinding heeft). Het afhandelen van een form is voornamelijk javascript en wanneer de javascript uit staat bij de eindgebruiker zou deze eigenlijk niet werken als dit clientside afgehandeld zou worden. Nu zullen de verzoeken altijd afgehandeld worden ondanks de situatie van de eindgebruiker.

export const actions = {
	default: async ({ request, fetch }) => {
		const data = await request.formData();
		const email = data.get('email');
		const message = data.get('message');
		const access_key = data.get('access_key');

		console.log('Received form data:', { email, message, access_key });

		const response = await fetch('https://api.web3forms.com/submit', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json'
			},
			body: JSON.stringify({
				access_key,
				email,
				message
			})
		});

		if (dev) {
			await new Promise((resolve) => setTimeout(resolve, 1000));
		}

		if (!response.ok) {
			const error = await response.json();
			console.error('Error from Web3Forms API:', error);
			return fail(response.status, { error: error.message || 'An error occurred' });
		}
 
		console.log('Form submitted successfully');
		return {
			success: true
		};
	}
};

Popover API (Progressive disclosure)

Ik heb de Popover API gebruikt om progressive disclosure te creΓ«ren. Progressive disclosure is een term die ik tijdens het meelopen bij CMD heb geleerd. Het houdt in dat je onnodige informatie weg laat op het scherm en laat vertonen wanneer deze wel relevant is voor de eindgebruiker. Tijdens CSS day heb ik geleerd over de Popover API. Dit is soort modal die opent doormiddel van een knop. Toen ik dit zag wist ik dat deze perfect zou zijn voor de description van de cards op mobile. Ik heb de Popover API als volgt gebruikt:

<button popovertarget="mypopover">Beschrijving...</button>
<div class="description" id="mypopover" popover>
	<PrismicRichText field={item.description} />
</div>

Media queries

Tijdens het bouwen van dit product heb ik gebruikt gemaakt van media queries om mijn product responsive te maken. Ik heb gebruik gemaakt van de min-width media query omdat ik mijn project mobile first heb gemaakt. De media queries geven mij de mogelijkheid om de de website responsive te maken. Een responsive design houdt in dat je op elke schermgrootte een website hebt dat een goede visuele hiΓ«rarchisch hanteert.

De website is dus mobile first gemaakt wat inhoudt dat de eerste code css gebouwd is op basis van een mobiele scherm. Vervolgens komt de eerste media query van @media (min-width: 960px), hierin stijl ik elementen die volgens de visuele hierarchie niet meer kloppen op een tablet scherm. Daarna komt de media query @media (min-width: 1200px), hierin stijl ik de volgende elementen die er niet meer goed uit zien op desktop/laptop scherm.

/* Mobile view code */

@media (min-width: 960px) {
/* Tablet view code */
}

@media (min-width: 1200px) {
/* Desktop view code */
}

Custom properties

Tijdens dit project heb ik gebruik gemaakt van custom properties. Custom properties zorgen ervoor dat in de styling de consistentie bewaakt wordt door de zelfde values elke keer mee te kunnen geven. Ook kan je de custom property veranderen en veranderd overal de value in de css. Mijn custom properties heb ik meegegeven in de global.css in de :root element en ziet er als volgt uit:

:root {
    --transition-duration: 0.3s;
    
    --primary-color: #680686;
    --primary-color-light: #9d4edd;
    --primary-color-dark: #3b003b;

    --secondary-color: #6b6565;
    --secondary-color-light: #D9D9D9;
    --secondary-color-dark: #3f3a3a;

    --accent-color: #A1CDF1;
    --accent-color-light: #D6EAF8;
    --accent-color-dark: #5DADE2;

    --background-color: #f5f5f5;

    --text-color: #333;
    --text-color-light: #666;
    --text-color-dark: #000;

    --primary-font-family: "Poppins", sans-serif;
}

Components

Sveltekit gecombineerd met prismic heeft van zichzelf een componentlogica die in je project geimplementeerd wordt. Zij maken gebruik van slices en components.

src/
β”œβ”€β”€ lib
β”‚   β”œβ”€β”€ atoms/
β”‚   β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ slices/
β”‚   β”œβ”€β”€ index.js
β”‚   β”œβ”€β”€ prismicio.js
β”œβ”€β”€ params/
β”œβ”€β”€ routes/

Omdat Prismic zijn eigen directory maakt met daarin de de components en slices zien deze er nu zo uit van binnen.

Components

De components zijn vergelijkbaar als de atoms in het atomic design. Dit zijn dus de kleinste components.

components/
β”œβ”€β”€ Bounded.svelte
β”œβ”€β”€ Header.svelte
β”œβ”€β”€ Heading.svelte
β”œβ”€β”€ PrismicRichText.svelte

Slices

De slices zijn vergelijkbaar als molecules (en soms organisms) van het atomic design. Dit zijn de wat grotere componenten.

slices/
β”œβ”€β”€ Contactform/
β”‚   β”œβ”€β”€ index.svelte
β”‚   β”œβ”€β”€ mocks.json
β”‚   β”œβ”€β”€ model.json
β”œβ”€β”€ Hero/
β”œβ”€β”€ HeroText/
β”œβ”€β”€ Image/
β”œβ”€β”€ ImageCards/
β”œβ”€β”€ ListTable/
β”œβ”€β”€ Quote/
β”œβ”€β”€ Text/
β”œβ”€β”€ TextWithImage
β”œβ”€β”€ ThreeGridLayout

Vervolgens worden ze meegegeven in de index.js (die ook automatisch gegenereerd wordt door Prismic en Sveltekit). Dat ziet er dan als volgt uit.

// Code generated by Slice Machine. DO NOT EDIT.

import Bulletpoints from './Bulletpoints/index.svelte';
import ContactForm from './ContactForm/index.svelte';
import Hero from './Hero/index.svelte';
import HeroText from './HeroText/index.svelte';
import Image from './Image/index.svelte';
import ImageCards from './ImageCards/index.svelte';
import ListTable from './ListTable/index.svelte';
import Quote from './Quote/index.svelte';
import Text from './Text/index.svelte';
import TextWithImage from './TextWithImage/index.svelte';
import ThreeGridLayout from './ThreeGridLayout/index.svelte';

export const components = {
	bulletpoints: Bulletpoints,
	contact_form: ContactForm,
	hero: Hero,
	hero_text: HeroText,
	image: Image,
	image_cards: ImageCards,
	list_table: ListTable,
	quote: Quote,
	text: Text,
	text_with_image: TextWithImage,
	three_grid_layout: ThreeGridLayout
};

Voorbeeld van semantiek

Ik heb een semantische form gemaakt op aangeven van docent joost.

<form method="POST" action="/" on:submit={handleSubmit} use:enhance>
	<fieldset>
		<legend>Kom in contact!</legend>
		<section>
			<label for="email">Email</label>
			<input required type="email" id="email" name="email" />
		</section>
		<section>
			<label for="phonenumber">Phonenumber</label>
			<input required type="tel" name="phonenumber" id="phonenumber" />
		</section>
		<section>
			<label for="message">Message</label>
			<textarea required name="message" id="message" minlength="2" maxlength="500" />
		</section>
		<section>
			<button class:submitted class:loading disabled={submitted}>
				{loading ? 'Loading' : 'Submit'}
			</button>
		</section>
	</fieldset>
</form>

Hieronder is mijn nav te zien waarbij ik gebruik heb gemaakt van de details element

<header>
	<PrismicLink field={navigation.data.home_link} class="Header__link text-xl font-semibold tracking-tight">
		<PrismicImage field={navigation.data.logo} width="200px" height="100%"/>
	</PrismicLink>
	<nav>
		<details open>
			<summary>
				<img src={HamburgerWhite} alt="Hamburger menu">
			</summary>
			<ul class="flex flex-wrap gap-6 md:gap-10">
				{#each navigation.data?.links as item}
					<li class="font-semibold tracking-tight text-slate-800">
						<PrismicLink field={item.link}>
							<PrismicText field={item.label} />
						</PrismicLink>
					</li>
				{/each}
			</ul>
		</details>
	</nav>
</header>

Accessability

Ik heb de volgende materie toegepast voor accessability

Performance

Om een high performance te waarborgen van de website heb ik de volgende technieken gebruikt:

Focus state

Om een duidelijke focus state te maken heb ik een extra dikke rode outline gebruikt zodat de gebruiker duidelijk ziet waar hij/zij is op de pagina. Hieronder is een voorbeeld te zien:

.grid > :global(a):focus {
	outline: 0.5rem solid #ff0000;
}

Loading

Voor de images die niet bij het inladen direct vertoond worden heb ik de loading="lazy" property gebruikt. Voor de images die bij het laden van de pagina direct vertoond worden op het scherm heb ik loading="eager" gedaan. Hieronder zijn voorbeelden in de code te zien van de loading property.

<PrismicImage
	field={slice.primary.backgroundImage}
	alt=""
	class="absolute inset-0 h-full w-full pointer-events-none select-none object-cover opacity-40"
	loading="eager"
/>

Bij loading = eager wordt er prioriteit gezet op de image die ingeladen wordt zodat deze eerder vertoond wordt op de browser van de gebruiker.

<PrismicImage field={item.image_logo} loading="lazy" />

Bij de loading = lazy wordt er een lagere prioriteit op de images gezet en worden deze pas later ingeladen wanneer ze het scherm van de gebruiker betreden.

Inline image sizing

Door inline in de image de height en width mee te geven zal de browser die exacte ruimte reserveren voor dat de content ingeladen wordt. Dit houdt in dat er geen sprake zal zijn van layout shifting en dat zal de performance ook weer verbeteren.

<PrismicLink field={navigation.data.home_link} class="text-xl font-semibold tracking-tight">
	<PrismicImage field={navigation.data.logo} width="200px" height="100%" />
</PrismicLink>

Img alt

Om de images op de freago site accessible te maken voor screenreaders heb ik een alt text toegevoegd aan de images. Dit is een voorbeeld van hoe ik dit heb toegepast:

<PrismicImage field={item.image_logo} loading="lazy" alt="foto van logo"/>

Aria

Om de links accessible te maken voor screenreaders heb ik een aria-label toegevoegd aan mijn links. Dit is een voorbeeld van hoe ik deze heb toegepast:

<li><PrismicLink field={item.website_link} aria-label="Ga verder naar de detailpagina">Soliciteer</PrismicLink></li>
<li><PrismicLink field={item.page_link} aria-label="Ga verder naar de website van {item.title}">Website</PrismicLink></li>

View transition API feature detection

Ik heb gebruik gemaakt van de view transition api. De view transition API zorgt ervoor dat je een smooth transition hebt van pagina naar pagina. Omdat een transition een beweging is, kan dit voor wat ongemak zorgen bij mensen die heel gevoelig zijn bij zulke bewegingen op het scherm. Wat ik hiervoor heb bedacht als oplossing is het gebruik van een feature detection. De feature detection merkt op of de gebruiker gebruikt maakt van de setting "reduced motion". Als de gebruiker deze aan heeft staan worden de view transitions gedeactiveerd.

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

Form (html css) validation

Op mijn website heb ik een form gemaakt. Deze heb ik progressively enhanced en daar vertel ik ook over een stukje terug op de pagina. Wat ik als eerst heb gedaan om de form client side accessible te maken is door de form te laten valideren met html en css elementen/properties. Ik zorg ervoor dat de velden die verplicht zijn een required property mee te geven in het html element. Daarnaast geef ik de html elementen een type mee zodat het element weet wat er in de input moet komen. Daarnaast geef ik het bij de input veld met de type=text een minimum lengte en een maximum lengte zodat gebruikers geen lege forms opsturen en ze ook niet een te lange bericht versturen. Daarnaast heb ik gebruik gemaakt van de :valid css property op de inputs. De inputs heeft nu een aantal vereisten waaraan het moet voldoen en op basis daarvan kan herkend worden of de input "valid" is. Als deze valid is kan ik een styling meegeven. Dit heb ik als volgt gedaan:

	#email:invalid,
	#message:invalid {
		outline: #ff0000 solid 2px;
	}

	#email:valid,
	#message:valid {
		outline: #00ff15 solid 2px;
	}

Fonts lokaal

Voor de performance heb ik de fonts gedownload en lokaal staan in een directory. Door de fonts te downloaden en lokaal te laten draaien scheelt dit de website tijd bij het moeten ophalen wanneer je deze importeert vanaf het internet. De fonts heb ik als volgt geimplementeerd in de code:

:root {
    --primary-font-family: "Poppins", sans-serif;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: var(--primary-font-family);
}

@font-face {
    font-family: "Poppins-Black";
    src: local("Poppins Black"), local("Poppins-Black"), 
        url("/fonts/static/fonts/Poppins-Black.ttf");
}

@font-face {
    font-family: "Poppins-Regular";
    src: local("Poppins Regular"), local("Poppins-Regular"), 
        url("/fonts/static/fonts/Poppins-Regular.ttf");
}

@font-face {
    font-family: "Poppins-Medium";
    src: local("Poppins Medium"), local("Poppins-Medium"), 
        url("/fonts/static/fonts/Poppins-Medium.ttf");
}

@font-face {
    font-family: "Poppins-Bold";
    src: local("Poppins Bold"), local("Poppins-Bold"), 
        url("/fonts/static/fonts/Poppins-Bold.ttf");
}

Live preview CMS

Met prismic heb je de optie om tijdens het maken van een page een live preview te zien. Dit is handig voor een content manager wanneer hij/zij een nieuwe page wilt maken en zien hoe deze eruit ziet voordat deze live gaat. Deze heb ik ingesteld door de volgende stappen te ondernemen:

1. Tutorial volgen van prismic

Prismic biedt een tutorial aan om de live preview aan te zetten. Deze heb ik gevolgd om te zien hoe dit werkt.

image

2. Vercel deployement

Om gebruik te mogen maken van de live preview moet je de website eerst live hebben staan. Ik maak gebruik van vercel dus ik deploy mijn repository op vercel voor een live URL.

image

3. Link kopiΓ«ren

Daarna kopieer ik de link met "/slice-simulator" in de slug. Deze exacte slug is nodig om de live preview mogelijk te maken in prismic. De normale url support hij niet.

image

4. Link zetten in prismic

Door de link in prismic te zetten voor de live preview kunnen we nu zien hoe de website eruit ziet als we te werk gaan in het CMS!

image

Github copilot

Ik heb github copilot ontdekt dankzij Marco. Dit is een nieuwe tool die ontworpen is om een AI je te laten helpen bij code vragen of moelijkheden. Het heeft mij in ieder geval geholpen voor CSS problemen en vragen. Daarnaast heb ik ook hulp gevraagd aan github copilot voor het werkend laten maken van de form. Het fijne van github copilot is dat hij ook een kleine uitleg geeft na zijn antwoord van hoe het werkt. Zo kan ik er zelf ook van leren en krijg ik ook goede code terug gestuurd. Het was nog wel de truc om je vragen goed op te stellen om zo ook een goed antwoord terug te krijgen. Hieroner is een voorbeeld van github copilot die mijn css DRY heeft gemaakt.

Voorbeeld vraag en antwoord

image

GSAP

Om de website te enhancen en aantrekkelijker te maken voor het oog, heb ik gebruik gemaakt van GSAP voor kleine simpele animaties. Door deze animaties toe te voegen krijgt de website iets meer spice en maakt het voor de gebruiker iets fijner om deze te gebruiken. Ik heb de volgende code gebruikt om een animatie toe te voegen aan de header bij het aankomen op de pagina.

import { onMount } from 'svelte';
import { gsap } from "gsap";

onMount(() => {
	const tl = gsap.timeline();
	const duration = 2;
	
	tl.from(".Header__link", {
		duration,
		opacity: 0,
        yPercent: -400
	})
	.from("details", {
		duration,
        opacity: 0,
        xPercent: 300,
		ease: 'power3.out',
	}, `-=${duration * 0.3}`)
	const checkWindowSize = () => {
		const detailsElement = document.querySelector('nav > details');
		if (detailsElement) {
			if (window.innerWidth >= 925) {
				detailsElement.setAttribute('open', '');
			} else {
				detailsElement.removeAttribute('open');
			}
		}
	};
	// Run once on mount
	checkWindowSize();
});
⚠️ **GitHub.com Fallback** ⚠️