3. Bouwen - Annevd/connect-your-tribe-squad-page GitHub Wiki

🟠 13 februari 2024: Mobile first & Scroll driven animations

Vandaag ben ik begonnen met het bouwen van mijn squadpage. Voor mobile first ben ik begonnen met een basic one column layout door middel van grid:

    display: grid;
    justify-content: center; 
    grid-template-columns: max-content;
    grid-gap: 3rem;

Omdat mij focus op desktop ligt heb ik het voor nu hierbij gelaten, als ik tijd over heb wil ik me graag nog verdiepen in hoe ik de mobile versie interessanter kan maken.

Om te beginnen voor desktop heb ik samen met Sanne geknutseld aan de HTML. Omdat ik voor de scroll animation 3 kolommen heb, heb ik 3 loops nodig in plaats van 1. Dit hebben we opgelost door drie keer een for loop te gebruiken.

De squadleden zijn ingedeeld in 3 <ul>'s met daarin elk één for loop.

Dit ziet er als volgt uit (met uitleg):

<div class="column-container">
      <%
      const endOfFirstColumn = Math.ceil(persons.length / 3); <!-- Dit berekent alle squadleden gedeeld door 3, om zo het einde van de eerste kolom te achterhalen -->
      const endOfSecondColumn = Math.ceil(persons.length * 2/3); <!-- Dit achterhaalt het einde van de tweede kolom -->
      const endOfThirdColumn = persons.length; <!-- Het einde van de derde kolom is de volledige lengte van de array -->
      %>

      <ul class="column column-reverse">
        <% for(var i=0; i < endOfFirstColumn; i++) { %> <!-- zolang i kleiner is dan de lengte van de eerste kolom, voer deze code uit en tel er elke keer een i bij op. Zo komen er dus i aantal li s -->
          <li>
            <img class="person-avatar" src="<%- persons[i].avatar %>" alt="avatar"> <p><%- persons[i].name %></p></li>
      <% } %>
      </ul>

      <ul class="column">
        <% for(var i = endOfFirstColumn; i < endOfSecondColumn; i++) { %> <!-- zolang i (in dit geval het einde van de eerste kolom) kleiner is dan de lengte van de tweede kolom, voer deze code uit en tel er elke keer een i bij op. Zo komen er dus i aantal li s -->
          <li>
            <img class="person-avatar" src="<%- persons[i].avatar %>" alt="avatar"> <p><%- persons[i].name %></p></li>
      <% } %>
      </ul>

      <ul class="column column-reverse">
        <% for(var i=endOfSecondColumn; i < endOfThirdColumn; i++) { %> <!-- zolang i (in dit geval het einde van de tweede kolom) kleiner is dan de lengte van de derde kolom, voer deze code uit en tel er elke keer een i bij op. Zo komen er dus i aantal li s -->
          <li>
            <img class="person-avatar" src="<%- persons[i].avatar %>" alt="avatar"> <p><%- persons[i].name %></p></li>
      <% } %>
      </ul>
  </div>

Om de scroll animation te maken heb ik 3 kolommen. Éen kolom is normaal en de andere twee geef ik een class waarmee ik ze ga reversen.

Ten eerste hebben alle drie de kolommen een overflow-y: hidden om de kaartjes die buiten de container vallen te verbergen. Dit doe ik ín de animation-timeline:

 @supports (animation-timeline: scroll()) {
        .columns {
            overflow-y: hidden;
        }
    }

Hetzelfde doe ik met flex-direction: column; om ervoor te zorgen dat de twee reverse kolommen omgekeerd worden:

@supports (animation-timeline: scroll()) {
    .column-reverse {
        flex-direction: column-reverse;
        }
    }

Als ik ze vervolgens buiten de animation-timeline padding geef, krijgen we een offset effect: de kaartjes staan niet meer recht naast elkaar.

.column {
        ...
        padding: 10vh 0;
    }

Om tot slot de scroll animatie te maken heb ik keyframes die ervoor zorgen dat de kaartjes bewegen als je scrollt:

@keyframes adjust-position {
0% {
    transform: translateY(calc(-100% + 100vh));
}
100% {
    transform: translateY(calc(100% - 100vh));
}
}

Deze roep ik vervolgens aan in de animation-timeline:

@supports (animation-timeline: scroll()) {
    .column-reverse {
        animation: adjust-position linear forwards;
        animation-timeline: scroll(root block);
        }
    }

Nu bewegen de 2 buitenste kolommen en de binnenste kolommen allebei een andere kant op als je scrollt!

🟡 14 februari 2024: Img fallback

Ik heb ervoor gezorgd dat als een squadlid geen avatar heeft of dat de avatar een error geeft, dat er een fallback foto is die in de plaats daarvan wordt weergeven. Dit heb ik zo in de EJS gedaan:

 <img src="<%- persons[i].avatar %> <%= (persons[i].avatar == '') ? 'images/fallback.png' : persons[i].avatar %>" onerror="this.src='images/fallback.png'" alt="avatar">

🟢 15 februari 2024: Detail page & custom properties

Vandaag heb ik belangrijke kleuren en andere waardes omgezet in custom properties voor een overzichtelijkere CSS.

Dit ziet er als volgt uit:

/* ----------- Custom properties ----------- */
:root {
    /* --- Glitch colors --- */
    --glitch-1: cyan;
    --glitch-2: #ff3b90;
    --glitch-3: yellow;
    --glitch-4: black;

    /* --- Font ---*/
    --primary-font: "Silkscreen", sans-serif;
    --secundary-font: "VT323", monospace;
    --tertiary-font: "DotGothic16", sans-serif;

    /* --- White space --- */
    --white-space-1: 1rem;
}

Daarnaast heb ik er voor gezorgd dat je op het kaartje kan klikken om naar een detail pagina van die persoon te gaan. Dit heb ik gedaan door een <a>om de <li>'s te zetten en als link href="./person/<%= persons[i].id %>" te zetten.

Op deze detailpagina is naast de naam ook hun bio te lezen en een link om naar hun Github te gaan.

🟣 26 februari 2024: Interactie Pt.1 + overig

Prefers-reduced-motion:

Om de website toegankelijker te maken voor mensen die niet houden van veel animaties en dergelijke heb ik in de css een prefers reduced motion optie gemaakt. Dit zorgt ervoor dat wanneer mensen deze optie aan hebben staan op hun apparaat dat er geen of minder animaties aanwezig zijn op de website.

In dit geval:

@media (prefers-reduced-motion) {
    .start-game {
        animation: none;
    }   
}

Bericht interactie:

Om ervoor te zorgen dat je comments kan achterlaten op de server ben ik begonnen met een variabele aan te maken in de server.js. Namelijk const messages = []. Hierin worden de comments opgeslagen.

In de ejs schrijf ik vervolgens de benodigde HTML en Javascript om door deze berichten heen te lopen:

<ul>
    <% messages.forEach(message => { %>
    <li class="message text-bubble left"><%- message %></li>
<% }) %>
</ul>

<form class="comment-form" method="POST" action="/person/<%- person.id %>">
    <label for="bericht">Laat een bericht achter!</label>
        <textarea class="text-bubble right input" required name="bericht"></textarea>
    <button class="submit-button" type="submit">Verstuur</button>
</form>

Terug naar de server.js, waar ik een POST aanmaak die de tekst data verstuurd en opslaat:

app.post('/person/:id', function (request, response) {
  // Er is nog geen afhandeling van POST, redirect naar GET op /
  messages.push(request.body.bericht) //bevat de tekst data/de berichten
  response.redirect(303, '/person/'+ request.params.id) // zorgt ervoor dat je op de huidige detail pagina blijft
})

Vervolgens voeg ik messages: messages toe als variabele:

// Maak een GET route voor een detailpagina met een request parameter id
app.get('/person/:id', function (request, response) {
  // Gebruik de request parameter id en haal de juiste persoon uit de WHOIS API op
  fetchJson(apiUrl + '/person/' + request.params.id).then((apiData) => {
    // Render person.ejs uit de views map en geef de opgehaalde data mee als variable, genaamd person
    response.render('person', {person: apiData.data, squads: squadData.data, messages: messages}) // zie hier
  })
})

EJS:

<h2 class="details-subtitle">Berichten:</h2>

<div class="messages-container">

<ul>
    <% messages.forEach(message => { %>
    <li class="message text-bubble left"><%- message %></li>
<% }) %>
</ul>

<form class="comment-form" method="POST" action="/person/<%- person.id %>">
    <label for="bericht">Laat een bericht achter!</label>
        <textarea class="text-bubble right input" required name="bericht"></textarea>
    <button name="verstuur" class="submit-button" type="submit">Verstuur</button>
</form>
</div>

CSS:

.messages-container {
    display: flex;
    flex-direction: column; 
    margin: 0 auto;
    padding: 1.5rem;
}

 .details-subtitle {
    text-align: center;
    font-size: 1.5rem;
}

.message {
    list-style: none;
    text-align: start;
}

.text-bubble {
    position: relative;
    display: block;
    margin: 20px;
    margin-bottom: 2rem;
    text-align: center;
    font-size: 16px;
    line-height:1.3em;
    background-color: white;
    color: black;
    padding: 12px;
    box-shadow: var(--bubble-border);
        
    box-sizing: border-box;
    width: fit-content;

    &::after {
        content: '';
        display: block;
        position: absolute;
        box-sizing: border-box;	
    }

    &.right::after {
		height: 4px;
		width: 4px;
		top: 84px;
		right: -8px;
		background: white;
		box-shadow: 
			4px -4px white,
			4px 0 white,
			8px 0 white,
			0 -8px white,
			4px 4px black, 
			8px 4px black, 
			12px 4px black, 
			16px 4px black,
			12px 0 black, 
			8px -4px black, 
			4px -8px black,
			0 -4px white;
	}

    &.left::after {

        height: 4px;
		width: 4px;
		top: 20px;
		left: -8px;
		background: white;
		box-shadow: 
			-4px -4px white,
			-4px 0 white,
			-8px 0 white,
			0 -8px white,
			-4px 4px black, 
			-8px 4px black, 
			-12px 4px black, 
			-16px 4px black,
			-12px 0 black, 
			-8px -4px black, 
			-4px -8px black,
			0 -4px white;
    }
}

textarea.input {
    border: none;
}

.comment-form {
    display: flex;
    flex-direction: column;
    justify-content: end;
    align-items: flex-end;
}

.submit-button {
    margin-right: 1rem;
    padding: 0.25rem 0.5rem;
    border: none;
    appearance: none;
    background: 14:20 28-2-2024
    box-shadow: var(--bubble-border-2);
    font-family: var(--primary-font);
    cursor: pointer;
}

Tot slot heb ik dit allemaal styling gegeven en ziet het er als volgt uit:

image

Dit werkt nu alleen op de server en de berichten worden nog niet opgeslagen in de API. Als ik de server heropstart verdwijnen deze dus weer. Hier ga ik nog aan werken, wordt vervolgd!

⚪ 27 februari 2024: Combineren

Vandaag heb ik ervoor gezorgd dat Daan's game nu ook te spelen is vanaf mijn homepage. Dit heb ik gedaan door de "Start Game" knop te linken naar de files van Daan die ik gekopieerd heb naar mijn eigen repo. Daarnaast heb ik de styling aangepast zodat het dezelfde stijl als mijn homepage heeft.

⚫ 28 februari 2024: Convergeren

Na het bespreken van hoe we verder gingen met convergeren heb ik de benodigde code van Daan in mijn code geplakt en deze dezelfde styling gegeven als de rest van mijn website. Je kan nu berichten naar een persoon sturen aan de hand van een popup. Als je de eerdere berichten popup opent en er nog geen berichten staan, staat er een zero state. Dit heb ik gedaan door een if else loop:

<% if (person.custom.messages && person.custom.messages.length) { %>
        <ul>
          <% person.custom.messages.forEach(message => { %>
            <li><%= message %></li>
          <% }) %>
          </ul>
      <% } else { %>
        <p>Er zijn nog geen berichten.</p>
      <% } %> 

Om de berichten ook echt op te slaan in de API heb ik onderstaande stappen doorlopen:

1. Maak een GET route voor de detailpagina:

app.get('/person/:id', function (request, response) {
  // Gebruik de request parameter id en haal de juiste persoon uit de WHOIS API op
  fetchJson(apiUrl + '/person/' + request.params.id).then((apiData) => {

    // Het custom field is een String, dus die moeten we eerst omzetten (= parsen)
    // naar een Object, zodat we er mee kunnen werken
    try {
      apiData.data.custom = JSON.parse(apiData.data.custom)
    } catch (error) {
      apiResponse.data.custom = {} // Als iemand nog geen custom data heeft, maak er dan een leeg object van
    }

    // Render person.ejs uit de views map en geef de opgehaalde data mee als variable, genaamd person
    response.render('person', {
      person: apiData.data,
      squads: squadData.data,
      messages: messages})
  })
})

2. Maak een POST route voor de detailpagina:

// Als we vanuit de browser een POST doen op de detailpagina van een persoon
app.post('/person/:id', function(request, response) {

  // Stap 1: Haal de huidige data op, zodat we altijd up-to-date zijn, en niks weggooien van anderen

  // Haal eerst de huidige gegevens voor deze persoon op, uit de WHOIS API
  fetchJson(apiUrl + '/person/' + request.params.id).then((apiResponse) => {

    // Het custom field is een String, dus die moeten we eerst
    // omzetten (= parsen) naar een Object, zodat we er mee kunnen werken
    try {
      apiResponse.data.custom = JSON.parse(apiResponse.data.custom)
    } catch (error) {
      apiResponse.data.custom = {}
    }

    // Stap 2: Gebruik de data uit het formulier
    // Deze stap zal voor iedereen net even anders zijn, afhankelijk van de functionaliteit

    // Controleer eerst welke actie is uitgevoerd, aan de hand van de submit button
    // Dit kan ook op andere manieren, of in een andere POST route
    if (request.body.actie == 'verstuur') {

      // Als het custom object nog geen messages Array als eigenschap heeft, voeg deze dan toe
      if (!apiResponse.data.custom.messages) {
        apiResponse.data.custom.messages = []
      }

      // Voeg een nieuwe message toe voor deze persoon, aan de hand van het bericht uit het formulier
      apiResponse.data.custom.messages.push(request.body.bericht)

    }

3. Sla de data op in de API:

// Voeg de nieuwe lijst messages toe in de WHOIS API,
    // via een PATCH request
    fetch(apiUrl + '/person/' + request.params.id, {
      method: 'PATCH',
      body: JSON.stringify({
        custom: apiResponse.data.custom,
      }),
      headers: {
        'Content-type': 'application/json; charset=UTF-8'
      }
    }).then((patchResponse) => {
      // Redirect naar de persoon pagina
      response.redirect(303, '/person/' + request.params.id)
    })
  })
})

⭕ 29 februari 2024: Sorteren

Om ervoor te zorgen dat je kan sorteren heb ik op het <form> method="POST" gebruikt om de resultaten door te kunnen geven aan de server. Als value bij de ```'s staat het laatste deel van de URL die je gebruikt om in de API te filteren.

<form method="POST" action="" class="sort-container">
  <label for="sort">Sorteren op</label>
  <select class="sort" name="sort" id="sort">
    <option value="name">a-z</option>
    <option value="-name">z-a</option>
    <option value="id">Id</option>
  </select>
  <button class="submit-button-sort" name="actie" value="verstuur" type="submit">kies</button>
</form>

Vervolgens maak ik in de server.js een post aan die al een deel van de gesorteerde URL fetched, + de value van de geselecteerde <option>.

app.post('/', function (request, response) {
  fetchJson(apiUrl + '/person/?filter={"squad_id":3}&sort=' + request.body.sort).then((persons) => {
    // apiData bevat gegevens van alle personen uit alle squads
    // Je zou dat hier kunnen filteren, sorteren, of zelfs aanpassen, voordat je het doorgeeft aan de view
    // Stap 3
    // Render index.ejs uit de views map en geef de opgehaalde data mee als variabele, genaamd persons

    // Stap 4
    // HTML maken op basis van JSON data
    response.render('index', {
      persons: persons.data,
      squads: squadData.data})
  })
})

Als je nu op een bepaald filter klikt, zal je naar de gesorteerde URL gestuurd worden.

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