Résume/CV Terminal

Contexte

Ca faisait un petit moment que je cherchais à réaliser un projet sympa qui soit en lien avec mon activité de développeur. J'ai donc penser à une façon original de représenter mon CV, le format sous forme de terminal m'a rapidement semblé répondre à ces critères. L'ensemble du code est disponible sur github.

Fonctionnement

Général

Lors de la réalisation de ce projet j'ai tout de suite voulu proposer quelques chose de facilement réadaptable en fonction du besoin. C'est pourquoi un fichier resume.json est présent: il contient l'ensemble des étapes à afficher.

[
  {
    "id": 1,
    "line": "curl https://adautry.fr/user/03101994", // La commande qui sera tapée
    "responseType": "code", // Le type de retour attendu, ici c'est du code
    "value": [ // La valeur affichée
      "{",
      "   \"name\" : \"Antoine DAUTRY\",",
      "   \"job\" : \"Fullstack developper\",",
      "   \"experience\" : \"3 years\",",
      "   \"location\" : \"Nantes, France\"",
      "}"
    ]
  },
  {
    "id": 2,
    "line": "tail experiences.html",
    "responseType": "table", // Le type de retour ici est un tableau
    "headers": [
      "Date",
      "Client",
      "Description",
      "Tech"
    ],
    "rows": [
      [
        "03/2021<br/>Now",
        "SII<br/><em>La Poste</em>",
        "Internal tool to schedule techniciens on interventions.<br/>Dashboard with BI data on technician productivity.",
        "Angular 11<br/>Spring Boot<br/>Spring Batch"
      ],
      [
        "02/2020<br/>03/2021",
        "SII<br/><em>I.T. Dept</em>",
        "Maintenance of a timesheet internal tool.<br/>Development of plugins for our ProjeQtor instance.<br/>Converting old webapp to Angular 9.",
        "Symfony<br/>Angular 9"
      ],
      [
        "11/2019<br/>02/2020",
        "SII<br/><em>Poste IMMO</em>",
        "Work on a process management tool to manage the evolution of a work request.",
        "Symfony 3.4<br/>AngularJS<br/>Processmaker"
      ]
    ]
  }
]

Il y a au moment ou j'écris ces lignes 4 types de retour possible :

  • list: Une liste qui sera affichée sous forme de puces
  • text: Qui sera simplement affiché comme du texte
  • code: Qui sera affiché avec un format monospace
  • table: Qui affiche un tableau

Il y a aussi un fichier de configuration permettant de régler le délai d'excécution, le délai entre chaque étape, etc.. :

{
  "delayBetweenSteps": 1500,// Délai entre chaque étape
  "enterDelay": 2000, // Délai d'exécution d'une commande
  "easterEggs" : true // Affiche des easters egg comme de la neige en décembre par exemple
}

Technique

Pour ce projet j'utilise du Javascript et parcel pour pouvoir générer le code finale.

Le fonctionnement est plutôt simple, dans un premier temps je lis la configuration et la liste des étapes afin d'avoir tout ça dans des variables. J'initialise les easter eggs si jamais ils sont activés.

L'étapes suivante consiste à convertir chaque étapes dans mon resume.json  en DOM et rajouter tout ça dans ma div qui a l'id terminal.

stepsJson.forEach((step) => {
    document.getElementById("terminal").append(getDom(step));
});

/**
 * Build DOM for a given step
 * @param {Step} step
 */
function getDom(step) {
    let html = "";

    if (step.responseType === "list" && Array.isArray(step.value)) {
        html = "<ul>";
        html += step.value.map((s) => `<li>${s}</li>`).join("");
        html += "</ul>";
    } else if (step.responseType === "text") {
        html = step.value;
    } else if (step.responseType === "table") {
        const headers = step.headers;
        const rows    = step.rows;
        const thsHtml = headers.map((h) => `<th>${h}</th>`).join("");
        const tdsHtml = rows
            .map((r) => `<tr>${r.map((rtd) => `<td>${rtd}</td>`).join("")}</tr>`)
            .join("");
        html          = `<table><thead><tr>${thsHtml}</tr></thead><tbody>${tdsHtml}</tbody></table>`;
    } else if (step.responseType === "code") {
        html = `<pre>${step.value.join('\n')}</pre>`;
    }
    return stringToDom(` <div id="terminal-line${step.id}" class="terminal__line" style="display: none;">
            <span id="line${step.id}" class="line"></span>
            <div id="line${step.id}-response" class="response">
                ${html}
            </div>
        </div>`);
}

Si jamais on a au moins une étape je la démarre pour que l'animation TypewriterJS commence :

// Starting terminal
if (stepsJson.length > 0) {
    play(0);
}

/**
 * Start typewriter for given step
 * @param {number} stepIndex Index of the step in the array stepsJson
 */
function play(stepIndex) {
    const step = stepsJson[stepIndex];
    document.getElementById(`terminal-line${step.id}`).style.display = "block";
    const betweenStepDelay = stepIndex === 0 ? 10 : config.delayBetweenSteps ?? 5000;
    scrollToBottomOfTerminalBody();

    new Typewriter(`#line${step.id}`, typewriterConfig)
        .pauseFor(betweenStepDelay)
        .typeString(step.line)
        .pauseFor(config.enterDelay ?? 2000)
        .callFunction(() => {
            // Hide cursor and show response
            document.querySelector(`#line${step.id} > .Typewriter__cursor`).style.display = "none";
            document.getElementById(`line${step.id}-response`).style.display = "block";

            if (hasNextStep(stepIndex)) {
                // There is an other step
                play(++stepIndex);
            } else {
                scrollToBottomOfTerminalBody();
            }
        })
        .start();
}

Si il y a une étape suivant, la fonction est rappelée avec l'index suivant.