Son CV dans un terminal web en Javascript!
https://terminal-cv.gregandev.fr
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
249 lines
7.7 KiB
249 lines
7.7 KiB
3 years ago
|
/**
|
||
|
* @typedef Command
|
||
|
* @property {string} command
|
||
|
* @property {string} responseType
|
||
|
* @property {string?} value
|
||
|
* @property {string[]?} headers
|
||
|
* @property {string[]?} rows
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @type {Command[]} commands
|
||
|
*/
|
||
|
import commands from "./resources/commands.json";
|
||
|
import { getCV, pif, rmRf, setDarkMode } from "./custom-comands";
|
||
|
import { dragElement } from "./draggable";
|
||
|
|
||
|
// Tableau contenant les commandes (utile pour la complétion des commandes)
|
||
|
let commandsList = [];
|
||
|
commands.forEach((c) => {
|
||
|
commandsList.push(c.command);
|
||
|
});
|
||
|
|
||
|
// Commandes qui nécessitent un traitement JS
|
||
|
const customCommands = ["clear", "dark", "light", "get cv"];
|
||
|
commandsList = commandsList.concat(customCommands);
|
||
|
|
||
|
// Commandes 'easter eggs' non disponibles à l'autocomplétion
|
||
|
const hiddenCommands = ["pif", "rm -rf /"];
|
||
|
|
||
|
// Ajout de la possibilité de déplacer la fenêtre pour les PC
|
||
|
if (window.innerWidth > 1024) {
|
||
|
dragElement(document.querySelector(".terminal"));
|
||
|
}
|
||
|
|
||
|
// Tableau contenant l'historique des commandes
|
||
|
const commandsHistory = [];
|
||
|
let historyMode = false;
|
||
|
let historyIndex = -1;
|
||
|
const terminalBody = document.querySelector(".terminal__body");
|
||
|
|
||
|
// Ajout de la ligne par défaut
|
||
|
addNewLine();
|
||
|
|
||
|
// Easter egg de décembre, ajout de flocons de neige
|
||
|
const now = new Date();
|
||
|
if (now.getMonth() === 11) {
|
||
|
let htmlFlakes = "";
|
||
|
for (let i = 0; i < 6; i++) {
|
||
|
htmlFlakes += `<div class="snowflake">❅</div><div class="snowflake">❆</div>`;
|
||
|
}
|
||
|
const html = `<div class="snowflakes" aria-hidden="true">${htmlFlakes}</div>`;
|
||
|
document.body.append(stringToDom(html));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retourne le HTML de la réponse pour une commande donnée
|
||
|
* @param {string} command
|
||
|
*/
|
||
|
function getDomForCommand(command) {
|
||
|
const commandObj = commands.find((el) => el.command === command);
|
||
|
let html = "";
|
||
|
if (commandObj === undefined) {
|
||
|
html = `'${
|
||
|
command.split(" ")[0]
|
||
|
}' n’est pas reconnu en tant que commande interne ou externe, un programme exécutable ou un fichier de commandes. Tapez la commande <code>help</code> pour afficher la liste des commandes disponibles.`;
|
||
|
} else {
|
||
|
if (commandObj.responseType === "list" && Array.isArray(commandObj.value)) {
|
||
|
html = "<ul>";
|
||
|
html += commandObj.value.map((s) => `<li>${s}</li>`).join("");
|
||
|
html += "</ul>";
|
||
|
} else if (commandObj.responseType === "text") {
|
||
|
html = commandObj.value;
|
||
|
} else if (commandObj.responseType === "table") {
|
||
|
const headers = commandObj.headers;
|
||
|
const rows = commandObj.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 (commandObj.responseType === "code") {
|
||
|
html = `<pre>${commandObj.value.join("\n")}</pre>`;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return html;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Ajoute une nouvelle ligne input de commande et désactive la précédente.
|
||
|
* @param {string|null} previousUid uid de la ligne précédente.
|
||
|
*/
|
||
|
function addNewLine(previousUid = null) {
|
||
|
const uid = Math.random().toString(36).replace("0.", "");
|
||
|
// terminal__line
|
||
|
const terminalLineEl = document.createElement("div");
|
||
|
terminalLineEl.classList.add("terminal__line");
|
||
|
|
||
|
// terminal__response
|
||
|
const terminalResponseEl = document.createElement("div");
|
||
|
terminalResponseEl.classList.add("terminal__response");
|
||
|
terminalResponseEl.id = `response-${uid}`;
|
||
|
|
||
|
// input text
|
||
|
const inputEl = document.createElement("input");
|
||
|
inputEl.type = "text";
|
||
|
inputEl.id = `input-${uid}`;
|
||
|
inputEl.autocapitalize = "off";
|
||
|
inputEl.dataset.uid = uid;
|
||
|
inputEl.dataset.active = "1"; // Utile pour le focus
|
||
|
inputEl.addEventListener("keydown", onCommandInput);
|
||
|
|
||
|
terminalLineEl.appendChild(inputEl);
|
||
|
if (previousUid) {
|
||
|
const previousInputEl = document.getElementById(previousUid);
|
||
|
if (previousInputEl) {
|
||
|
previousInputEl.setAttribute("disabled", "true");
|
||
|
previousInputEl.removeEventListener("keydown", onCommandInput);
|
||
|
delete previousInputEl.dataset.active;
|
||
|
}
|
||
|
}
|
||
|
document.getElementById("terminal").appendChild(terminalLineEl);
|
||
|
document.getElementById("terminal").appendChild(terminalResponseEl);
|
||
|
|
||
|
inputEl.focus(); // Ajoute le focus dès la création du champs
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gère le keydown sur l'input de la commande.
|
||
|
* @param e
|
||
|
*/
|
||
|
function onCommandInput(e) {
|
||
|
const commandValue = e.target.value.trim().toLowerCase();
|
||
|
if (e.keyCode === 13) {
|
||
|
// ENTER
|
||
|
if (commandValue !== "") {
|
||
|
historyMode = false;
|
||
|
const idResponse = `response-${e.target.dataset.uid}`;
|
||
|
const responseEl = document.getElementById(idResponse);
|
||
|
let html;
|
||
|
if (
|
||
|
hiddenCommands.includes(commandValue) ||
|
||
|
customCommands.includes(commandValue)
|
||
|
) {
|
||
|
html = handleCustomCommands(commandValue);
|
||
|
} else {
|
||
|
html = getDomForCommand(commandValue);
|
||
|
}
|
||
|
if (responseEl) {
|
||
|
responseEl.innerHTML = html;
|
||
|
commandsHistory.push(commandValue);
|
||
|
addNewLine(e.target.id);
|
||
|
}
|
||
|
}
|
||
|
} else if (e.keyCode === 9) {
|
||
|
// TAB
|
||
|
e.preventDefault();
|
||
|
if (commandValue === "") {
|
||
|
this.value = "help";
|
||
|
} else {
|
||
|
const matchingCommand = commandsList.find((c) =>
|
||
|
c.startsWith(commandValue)
|
||
|
);
|
||
|
if (matchingCommand) {
|
||
|
this.value = matchingCommand;
|
||
|
}
|
||
|
}
|
||
|
historyMode = false;
|
||
|
} else if (e.keyCode === 38 || e.keyCode === 40) {
|
||
|
// UP / DOWN
|
||
|
// Gestion de l'historique
|
||
|
if (commandsHistory.length > 0) {
|
||
|
if (historyMode === false) {
|
||
|
historyIndex = commandsHistory.length - 1;
|
||
|
} else {
|
||
|
if (e.keyCode === 38 && historyIndex !== 0) {
|
||
|
// UP
|
||
|
historyIndex--;
|
||
|
} else if (
|
||
|
e.keyCode === 40 &&
|
||
|
historyIndex !== commandsHistory.length - 1
|
||
|
) {
|
||
|
historyIndex++;
|
||
|
}
|
||
|
}
|
||
|
this.value = commandsHistory[historyIndex];
|
||
|
}
|
||
|
historyMode = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Permet de gérer les commandes cachées (non proposées dans l'autocomplétion)
|
||
|
* @param {string} command
|
||
|
* @returns {string|void} Html à afficher dans la réponse de la commande
|
||
|
*/
|
||
|
function handleCustomCommands(command) {
|
||
|
switch (command) {
|
||
|
case "pif":
|
||
|
pif();
|
||
|
return "C'est la fête !";
|
||
|
case "light":
|
||
|
if (document.body.classList.length === 0)
|
||
|
return "Vous êtes déjà en mode clair";
|
||
|
setDarkMode(false);
|
||
|
return "Vous êtes maintenant en mode clair.";
|
||
|
case "dark":
|
||
|
if (document.body.classList.length === 1)
|
||
|
return "Vous êtes déjà en mode sombre";
|
||
|
setDarkMode(true);
|
||
|
return "Vous êtes maintenant en mode sombre.";
|
||
|
case "get cv":
|
||
|
getCV();
|
||
|
return "Le CV va être téléchargé.";
|
||
|
case "rm -rf /":
|
||
|
rmRf();
|
||
|
return "w4dhIHZhIFDDiVRFUiAh";
|
||
|
case "clear":
|
||
|
terminalBody.innerHTML = `<div id="terminal"></div>`;
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Convert HTML to DOM object
|
||
|
* @param html
|
||
|
* @returns {DocumentFragment}
|
||
|
*/
|
||
|
function stringToDom(html) {
|
||
|
return document.createRange().createContextualFragment(html);
|
||
|
}
|
||
|
|
||
|
// ------------------------------------------------------------------------------------
|
||
|
// EVENT LISTENNER
|
||
|
// ------------------------------------------------------------------------------------
|
||
|
|
||
|
// Ajout du focus sur l'input même si on clique sur le body (pour garder le curseur)
|
||
|
document.body.addEventListener("click", function (e) {
|
||
|
if (e.target.tagName !== "INPUT") {
|
||
|
const activeInput = document.querySelector("input[data-active]");
|
||
|
activeInput.focus();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
document.querySelector(".fake-close").addEventListener("click", function (e) {
|
||
|
const terminalEl = document.querySelector(".terminal");
|
||
|
terminalEl.parentElement.removeChild(terminalEl);
|
||
|
});
|