3. Bouwen - Trisjan/Freago GitHub Wiki
Ik heb gekozen voor Vercel.
Ik heb een form gebouwd die progressive enhanced is om zo de belangen van de eindgebruiker te bewaren.
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>
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
};
}
};
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>
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 */
}
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;
}
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
};
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>
Ik heb de volgende materie toegepast voor accessability
Om een high performance te waarborgen van de website heb ik de volgende technieken gebruikt:
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;
}
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.
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>
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"/>
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>
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;
}
}
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;
}
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");
}
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:
Prismic biedt een tutorial aan om de live preview aan te zetten. Deze heb ik gevolgd om te zien hoe dit werkt.
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.
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.
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!
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
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();
});