et un push ma foi!
This commit is contained in:
commit
bc3671ff60
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@ -0,0 +1,16 @@
|
||||
FROM debian:bullseye-slim as builder
|
||||
WORKDIR /data
|
||||
COPY . .
|
||||
RUN apt update && apt install -y npm
|
||||
RUN npm install -i package.json \
|
||||
&& npm run build
|
||||
|
||||
FROM alpine
|
||||
|
||||
RUN apk update \
|
||||
&& apk add lighttpd \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
COPY --from=builder /data/dist /var/www/localhost/htdocs
|
||||
|
||||
CMD ["lighttpd","-D","-f","/etc/lighttpd/lighttpd.conf"]
|
21
LICENCE
Normal file
21
LICENCE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Antoine DAUTRY
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
134
README.md
Normal file
134
README.md
Normal file
@ -0,0 +1,134 @@
|
||||
# Resume Terminal
|
||||
|
||||
This project is forked from https://github.com/antoine1003/resume-terminal
|
||||
|
||||
## About
|
||||
|
||||
This projet use [ParcelJS](https://parceljs.org/) as build tool. It is made from scratch, the only library used is for an hidden command `paf` [canvas-confetti](https://github.com/catdad/canvas-confetti).
|
||||
|
||||
## get started
|
||||
|
||||
### installation
|
||||
|
||||
- Install npm:
|
||||
```bash
|
||||
sudo apt install npm
|
||||
```
|
||||
|
||||
- Install dependencies (from project root folder):
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### usage
|
||||
|
||||
- To run in dev mode: `
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- To build for production:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## config
|
||||
|
||||
### commands.json
|
||||
|
||||
File `commands.json` contain all commands that just needs to display simple data and doesn't need a JS actions.
|
||||
|
||||
For now, there are 4 possible type of steps :
|
||||
- list
|
||||
- text
|
||||
- code
|
||||
- table
|
||||
|
||||
#### responseType = list
|
||||
|
||||
To display a bullet list, the `value` field is an array of string.
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "whois adautry",
|
||||
"responseType": "list",
|
||||
"value": [
|
||||
"A 27 years old full stack developper",
|
||||
"3 years of experiences",
|
||||
"Living in Nantes"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### responseType = table
|
||||
|
||||
Display a table, this object requires two fields :
|
||||
|
||||
- `headers`: Headers of the array
|
||||
- `rows`: Array containing rows
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "whereis experiences",
|
||||
"responseType": "table",
|
||||
"headers": [
|
||||
"Date",
|
||||
"Client",
|
||||
"Description",
|
||||
"Tech"
|
||||
],
|
||||
"rows": [
|
||||
[
|
||||
"2021",
|
||||
"La Poste",
|
||||
"Internal tool to schedule techniciens on interventions.",
|
||||
"Angular 11, Spring Boot/Batch, Genetic algorithm"
|
||||
],
|
||||
[
|
||||
"2020",
|
||||
"DSI",
|
||||
"Maintenance of a timesheet internal tool. Development of plugins for our ProjeQtor instance.",
|
||||
"Symfony, Angular 8"
|
||||
]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### responseType = text
|
||||
|
||||
Just display text contained in `value`.
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "find . -type f -print | xargs grep \"hobby\"",
|
||||
"responseType": "text",
|
||||
"value": "Bonsoir"
|
||||
}
|
||||
```
|
||||
|
||||
#### responseType = code
|
||||
|
||||
Display code between `pre` tag, `value` is an array of string, each string is a line.
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "curl https://adautry.fr/user/03101994",
|
||||
"responseType": "code",
|
||||
"value": [
|
||||
"{",
|
||||
" \"name\":\"Antoine DAUTRY\",",
|
||||
" \"job\":\"Fullstack developper\",",
|
||||
" \"experience\":\"3 years\",",
|
||||
" \"city\":\"Nantes\"",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Customs commands
|
||||
|
||||
In the `app.js` file you can see multiple arrays that stores commands :
|
||||
|
||||
- `hiddenCommands`: Commands that are not use in autocompletion (easter egg commands for example)
|
||||
- `customCommands`: Commands that needs a specials JS treatments, in my case `dark`/`light` to swith app theme, `get cv` to download my resume, ...
|
||||
- `commandsList`: This is the main array used for autocompletion, it stores `customCommands` **and** commands that are listed in the `commands.json` file.
|
5206
package-lock.json
generated
Normal file
5206
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "cv-terminal",
|
||||
"version": "2.1.2",
|
||||
"description": "Nice looking resume.",
|
||||
"scripts": {
|
||||
"dev": "npx parcel src/index.html",
|
||||
"build": "npx parcel build src/index.html"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@parcel/config-default": "^2.3.2",
|
||||
"canvas-confetti": "^1.5.1",
|
||||
"fireworks-js": "^1.3.5",
|
||||
"postcss": "^8.3.11"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@parcel/packager-raw-url": "^2.3.2",
|
||||
"@parcel/transformer-sass": "^2.3.2",
|
||||
"@parcel/transformer-webmanifest": "^2.3.2",
|
||||
"cssnano": "^5.0.8",
|
||||
"parcel": "^2.3.2",
|
||||
"parcel-reporter-static-files-copy": "^1.3.4",
|
||||
"prettier": "2.4.1",
|
||||
"sass": "^1.43.4"
|
||||
},
|
||||
"staticFiles": {
|
||||
"staticOutPath": "resources"
|
||||
}
|
||||
}
|
BIN
src/android-chrome-192x192.png
Normal file
BIN
src/android-chrome-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
BIN
src/android-chrome-512x512.png
Normal file
BIN
src/android-chrome-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
248
src/app.js
Normal file
248
src/app.js
Normal file
@ -0,0 +1,248 @@
|
||||
/**
|
||||
* @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);
|
||||
});
|
BIN
src/apple-touch-icon.png
Normal file
BIN
src/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.9 KiB |
67
src/custom-comands.js
Normal file
67
src/custom-comands.js
Normal file
@ -0,0 +1,67 @@
|
||||
import confetti from "canvas-confetti";
|
||||
import { Fireworks } from "fireworks-js";
|
||||
|
||||
/**
|
||||
* Affiche des confettis sur la page
|
||||
*/
|
||||
export function pif() {
|
||||
const count = 200;
|
||||
const defaults = {
|
||||
origin: { y: 0.7 },
|
||||
};
|
||||
|
||||
function fire(particleRatio, opts) {
|
||||
confetti(
|
||||
Object.assign({}, defaults, opts, {
|
||||
particleCount: Math.floor(count * particleRatio),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fire(0.25, {
|
||||
spread: 26,
|
||||
startVelocity: 55,
|
||||
});
|
||||
fire(0.2, {
|
||||
spread: 60,
|
||||
});
|
||||
fire(0.35, {
|
||||
spread: 100,
|
||||
decay: 0.91,
|
||||
scalar: 0.8,
|
||||
});
|
||||
fire(0.1, {
|
||||
spread: 120,
|
||||
startVelocity: 25,
|
||||
decay: 0.92,
|
||||
scalar: 1.2,
|
||||
});
|
||||
fire(0.1, {
|
||||
spread: 120,
|
||||
startVelocity: 45,
|
||||
});
|
||||
}
|
||||
|
||||
export function setDarkMode(value) {
|
||||
if (value) {
|
||||
document.body.classList.add("dark-mode");
|
||||
} else {
|
||||
document.body.classList.remove("dark-mode");
|
||||
}
|
||||
}
|
||||
|
||||
export function getCV() {
|
||||
const a = document.createElement("a");
|
||||
a.href = "resources/CV - Greg Lebreton.pdf";
|
||||
a.setAttribute("download", "CV - Greg Lebreton.pdf.pdf");
|
||||
a.click();
|
||||
}
|
||||
|
||||
export function rmRf() {
|
||||
setDarkMode(true);
|
||||
document.body.classList.add("firework");
|
||||
const fireworks = new Fireworks(document.body, {
|
||||
mouse: { click: true, move: false, max: 7 },
|
||||
});
|
||||
fireworks.start();
|
||||
}
|
44
src/draggable.js
Normal file
44
src/draggable.js
Normal file
@ -0,0 +1,44 @@
|
||||
export function dragElement(elmnt) {
|
||||
var pos1 = 0,
|
||||
pos2 = 0,
|
||||
pos3 = 0,
|
||||
pos4 = 0;
|
||||
const element = document.querySelector(".terminal__header");
|
||||
if (element) {
|
||||
// if present, the header is where you move the DIV from:
|
||||
element.onmousedown = dragMouseDown;
|
||||
} else {
|
||||
// otherwise, move the DIV from anywhere inside the DIV:
|
||||
elmnt.onmousedown = dragMouseDown;
|
||||
}
|
||||
|
||||
function dragMouseDown(e) {
|
||||
e = e || window.event;
|
||||
e.preventDefault();
|
||||
// get the mouse cursor position at startup:
|
||||
pos3 = e.clientX;
|
||||
pos4 = e.clientY;
|
||||
document.onmouseup = closeDragElement;
|
||||
// call a function whenever the cursor moves:
|
||||
document.onmousemove = elementDrag;
|
||||
}
|
||||
|
||||
function elementDrag(e) {
|
||||
e = e || window.event;
|
||||
e.preventDefault();
|
||||
// calculate the new cursor position:
|
||||
pos1 = pos3 - e.clientX;
|
||||
pos2 = pos4 - e.clientY;
|
||||
pos3 = e.clientX;
|
||||
pos4 = e.clientY;
|
||||
// set the element's new position:
|
||||
elmnt.style.top = elmnt.offsetTop - pos2 + "px";
|
||||
elmnt.style.left = elmnt.offsetLeft - pos1 + "px";
|
||||
}
|
||||
|
||||
function closeDragElement() {
|
||||
// stop moving when mouse button is released:
|
||||
document.onmouseup = null;
|
||||
document.onmousemove = null;
|
||||
}
|
||||
}
|
BIN
src/favicon-16x16.png
Normal file
BIN
src/favicon-16x16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 610 B |
BIN
src/favicon-32x32.png
Normal file
BIN
src/favicon-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
src/favicon.ico
Normal file
BIN
src/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
60
src/index.html
Normal file
60
src/index.html
Normal file
@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" name="viewport"/>
|
||||
<meta content="ie=edge" http-equiv="X-UA-Compatible"/>
|
||||
<meta content="Greg Lebreton - Devops Engineer" name="title"/>
|
||||
<!--meta content="Mon CV sous la forme d'un terminal. Parce qu'il n'y a pas de raisons que seuls les développeurs utilisent un terminal ! :D" name="description"/-->
|
||||
<meta content="resume,dautry,developer,fullstack,france" name="keywords"/>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
|
||||
<meta content="Français" name="language"/>
|
||||
<!--meta content="Antoine DAUTRY" name="author"/-->
|
||||
<title>Terminal CV</title>
|
||||
<script src="app.js" type="module" defer></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono&display=swap" rel="stylesheet">
|
||||
<link href="scss/style.scss" rel="stylesheet">
|
||||
<link crossorigin="anonymous" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"
|
||||
integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ=="
|
||||
referrerpolicy="no-referrer" rel="stylesheet"/>
|
||||
<link href="apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180">
|
||||
<link href="favicon-32x32.png" rel="icon" sizes="32x32" type="image/png">
|
||||
<link href="favicon-16x16.png" rel="icon" sizes="16x16" type="image/png">
|
||||
<link href="site.webmanifest" rel="manifest">
|
||||
</head>
|
||||
<body>
|
||||
<div class="terminal">
|
||||
<div class="terminal__header">
|
||||
<div class="fake-button fake-close"></div>
|
||||
<div class="fake-button fake-minimize"></div>
|
||||
<div class="fake-button fake-zoom"></div>
|
||||
</div>
|
||||
<div class="terminal__body">
|
||||
<div class="terminal__banner"><pre>
|
||||
██████╗ ██╗ ██╗ ████████╗███████╗██████╗ ███╗ ███╗██╗███╗ ██╗ █████╗ ██╗
|
||||
██╔════╝██║ ██║ ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██║████╗ ██║██╔══██╗██║
|
||||
██║ ██║ ██║ ██║ █████╗ ██████╔╝██╔████╔██║██║██╔██╗ ██║███████║██║
|
||||
██║ ╚██╗ ██╔╝ ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║██║██║╚██╗██║██╔══██║██║
|
||||
╚██████╗ ╚████╔╝ ██║ ███████╗██║ ██║██║ ╚═╝ ██║██║██║ ╚████║██║ ██║███████╗
|
||||
╚═════╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝</pre>
|
||||
<div class="terminal__author">Greg Lebreton</div>
|
||||
<p>Bienvenue sur mon CV terminal ! Pour afficher les commandes disponibles tapez <code>help</code>. Pour valider chaque commande appuyez sur <em>Entrer</em>, vous pouvez utiliser la touche <em>Tabulation</em> afin de vous aider à compléter une commande.</p>
|
||||
</div>
|
||||
<div id="terminal"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="socials">
|
||||
<!--a href="https://www.linkedin.com/in/antoine-dautry/" target="_blank"-->
|
||||
<i class="fab fa-linkedin"></i>
|
||||
</a>
|
||||
</div>
|
||||
<!--a href="https://github.com/antoine1003/resume-terminal" id="banner-github" target="_blank">
|
||||
<img alt="Fork me on GitHub" height="149" loading="lazy"
|
||||
src="https://github.blog/wp-content/uploads/2008/12/forkme_right_darkblue_121621.png?resize=149%2C149"
|
||||
width="149"/>
|
||||
</a-->
|
||||
</body>
|
||||
</html>
|
87
src/resources/commands.json
Normal file
87
src/resources/commands.json
Normal file
@ -0,0 +1,87 @@
|
||||
[
|
||||
{
|
||||
"command":"help",
|
||||
"responseType":"list",
|
||||
"value":[
|
||||
"<code>a-propos</code> : Affiche les informations me concernant",
|
||||
"<code>clear</code> : Nettoie le terminal",
|
||||
"<code>experiences</code> : Affiche la liste de mes expériences",
|
||||
"<code>get cv</code> : Télécharge le CV",
|
||||
"<code>help</code> : Affiche l'aide",
|
||||
"<code>hobby</code> : Affiche la liste de mes passes temps",
|
||||
"<code>projets-perso</code> : Affiche la liste de mes projets personnels",
|
||||
"<code>dark/light</code> : Change le thème de la page",
|
||||
"<em>Vous pouvez utiliser la touche TAB afin de compléter une commande</em>",
|
||||
"<em>Vous pouvez retrouver les anciennes commandes avec les flèches haut et bas.</em>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command":"a-propos",
|
||||
"responseType":"code",
|
||||
"value":[
|
||||
"{",
|
||||
" \"nom\" : \"Gregory Lebreton\",",
|
||||
" \"poste\" : \"Ingénieur Devops\",",
|
||||
" \"experience\" : \"5\",",
|
||||
" \"ville\" : \"Paris, France\"",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command":"experiences",
|
||||
"responseType":"table",
|
||||
"headers":[
|
||||
"Date",
|
||||
"Client",
|
||||
"Description",
|
||||
"Tech"
|
||||
],
|
||||
"rows":[
|
||||
[
|
||||
"06/2019<br/>09/2019",
|
||||
"<br/><em>Safran, S.A.E</em>",
|
||||
"Mise en place d'une plateforme mettant en relation les<br/>différents acteurs de la DSI sur une plateforme logicielle.",
|
||||
"Docker<br/>Kubernetes<br/>Axelor"
|
||||
],
|
||||
[
|
||||
"12/2017<br/>03/2019",
|
||||
"PHP dev<br/><em>Leading Frog</em>",
|
||||
"Module PHP permettant l'envoie de cartes postales<br/>numériques avec implémentation API Stripe.",
|
||||
"PHP<br/>JavaScript<br/>SQL"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"command":"hobby",
|
||||
"responseType":"list",
|
||||
"value":[
|
||||
"Musique: Skateboard, Unity, VR",
|
||||
"Programmation: Python, bash, PHP",
|
||||
"Autre: Cinéma, Environnement, Famille"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command":"projets-perso",
|
||||
"responseType":"table",
|
||||
"headers":[
|
||||
"Nom",
|
||||
"Description",
|
||||
"Tech",
|
||||
"Liens"
|
||||
],
|
||||
"rows":[
|
||||
[
|
||||
"Personal website<br/>(2021)",
|
||||
"Site web personnel me permettant de montrer mes projets et tester des applicatifs<br/>",
|
||||
"PHP/JS",
|
||||
"<a href=\"https://www.gregandev.fr\" target=\"blank\">Lien</a>"
|
||||
],
|
||||
[
|
||||
"GoldeneyeVR<br/>(2020)",
|
||||
"Implémentation VR au célèbre jeux de 1997.",
|
||||
"C# WPF",
|
||||
"<a href=\"https://www.gregandev.fr/index.php?contenu=GoldeneyeProjet\" target=\"blank\">Lien</a>"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
137
src/scss/_snowflakes.scss
Normal file
137
src/scss/_snowflakes.scss
Normal file
@ -0,0 +1,137 @@
|
||||
.snowflake {
|
||||
color: #fff;
|
||||
font-size: 1em;
|
||||
font-family: Arial, sans-serif;
|
||||
text-shadow: 0 0 5px #000;
|
||||
}
|
||||
|
||||
@-webkit-keyframes snowflakes-fall {
|
||||
0% {
|
||||
top: -10%
|
||||
}
|
||||
100% {
|
||||
top: 100%
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes snowflakes-shake {
|
||||
0%, 100% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0)
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: translateX(80px);
|
||||
transform: translateX(80px)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes snowflakes-fall {
|
||||
0% {
|
||||
top: -10%
|
||||
}
|
||||
100% {
|
||||
top: 100%
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes snowflakes-shake {
|
||||
0%, 100% {
|
||||
transform: translateX(0)
|
||||
}
|
||||
50% {
|
||||
transform: translateX(80px)
|
||||
}
|
||||
}
|
||||
|
||||
.snowflake {
|
||||
position: fixed;
|
||||
top: -10%;
|
||||
z-index: 9999;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
-webkit-animation-name: snowflakes-fall, snowflakes-shake;
|
||||
-webkit-animation-duration: 10s, 3s;
|
||||
-webkit-animation-timing-function: linear, ease-in-out;
|
||||
-webkit-animation-iteration-count: infinite, infinite;
|
||||
-webkit-animation-play-state: running, running;
|
||||
animation-name: snowflakes-fall, snowflakes-shake;
|
||||
animation-duration: 10s, 3s;
|
||||
animation-timing-function: linear, ease-in-out;
|
||||
animation-iteration-count: infinite, infinite;
|
||||
animation-play-state: running, running
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(0) {
|
||||
left: 1%;
|
||||
-webkit-animation-delay: 0s, 0s;
|
||||
animation-delay: 0s, 0s
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(1) {
|
||||
left: 10%;
|
||||
-webkit-animation-delay: 1s, 1s;
|
||||
animation-delay: 1s, 1s
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(2) {
|
||||
left: 20%;
|
||||
-webkit-animation-delay: 6s, .5s;
|
||||
animation-delay: 6s, .5s
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(3) {
|
||||
left: 30%;
|
||||
-webkit-animation-delay: 4s, 2s;
|
||||
animation-delay: 4s, 2s
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(4) {
|
||||
left: 40%;
|
||||
-webkit-animation-delay: 2s, 2s;
|
||||
animation-delay: 2s, 2s
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(5) {
|
||||
left: 50%;
|
||||
-webkit-animation-delay: 8s, 3s;
|
||||
animation-delay: 8s, 3s
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(6) {
|
||||
left: 60%;
|
||||
-webkit-animation-delay: 6s, 2s;
|
||||
animation-delay: 6s, 2s
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(7) {
|
||||
left: 70%;
|
||||
-webkit-animation-delay: 2.5s, 1s;
|
||||
animation-delay: 2.5s, 1s
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(8) {
|
||||
left: 80%;
|
||||
-webkit-animation-delay: 1s, 0s;
|
||||
animation-delay: 1s, 0s
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(9) {
|
||||
left: 90%;
|
||||
-webkit-animation-delay: 3s, 1.5s;
|
||||
animation-delay: 3s, 1.5s
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(10) {
|
||||
left: 25%;
|
||||
-webkit-animation-delay: 2s, 0s;
|
||||
animation-delay: 2s, 0s
|
||||
}
|
||||
|
||||
.snowflake:nth-of-type(11) {
|
||||
left: 65%;
|
||||
-webkit-animation-delay: 4s, 2.5s;
|
||||
animation-delay: 4s, 2.5s
|
||||
}
|
314
src/scss/style.scss
Normal file
314
src/scss/style.scss
Normal file
@ -0,0 +1,314 @@
|
||||
$border-radius: 5px;
|
||||
|
||||
:root {
|
||||
--text-color: #fff;
|
||||
--text-accent-color: darksalmon;
|
||||
--link-color: darkorange;
|
||||
--bg-1: #f27121;
|
||||
--bg-2: #e94057;
|
||||
--bg-3: #8a2387;
|
||||
--bg-1-social: #f3a183;
|
||||
--bg-2-social: #ec6f66;
|
||||
--username-color: cadetblue;
|
||||
--terminal-bg: rgba(56, 4, 40, 0.9);
|
||||
--terminal-header-bg: #bbb;
|
||||
}
|
||||
|
||||
body {
|
||||
&.dark-mode {
|
||||
--text-accent-color: #ffca85;
|
||||
--link-color: burlywood;
|
||||
--bg-1: #211F20;
|
||||
--bg-2: #292D34;
|
||||
--bg-3: #213030;
|
||||
--bg-1-social: #414141;
|
||||
--bg-2-social: #485461;
|
||||
--username-color: #858585;
|
||||
--terminal-bg: rgb(0 0 0 / 90%);
|
||||
--terminal-header-bg: #585252;
|
||||
&.firework {
|
||||
--terminal-bg: rgb(0 0 0 / 15%);
|
||||
}
|
||||
}
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: var(--bg-3); /* fallback for old browsers */
|
||||
background: -webkit-linear-gradient(
|
||||
to right,
|
||||
var(--bg-1),
|
||||
var(--bg-2),
|
||||
var(--bg-3)
|
||||
); /* Chrome 10-25, Safari 5.1-6 */
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--bg-1),
|
||||
var(--bg-2),
|
||||
var(--bg-3)
|
||||
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
position: absolute;
|
||||
resize: both;
|
||||
overflow: hidden;
|
||||
height: 450px;
|
||||
width: min(900px, 90vw);
|
||||
|
||||
.terminal__header {
|
||||
height: 25px;
|
||||
padding: 0 8px;
|
||||
background-color: var(--terminal-header-bg);
|
||||
margin: 0 auto;
|
||||
border-top-right-radius: $border-radius;
|
||||
border-top-left-radius: $border-radius;
|
||||
cursor: move;
|
||||
|
||||
.fake-button {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #000;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
&.fake-close {
|
||||
left: 6px;
|
||||
background-color: #ff3b47;
|
||||
border-color: #9d252b;
|
||||
}
|
||||
|
||||
&.fake-minimize {
|
||||
left: 11px;
|
||||
background-color: #ffc100;
|
||||
border-color: #9d802c;
|
||||
}
|
||||
|
||||
&.fake-zoom {
|
||||
left: 16px;
|
||||
background-color: #00d742;
|
||||
border-color: #049931;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.terminal__body {
|
||||
font-family: "Ubuntu Mono", monospace;
|
||||
background: var(--terminal-bg);
|
||||
color: var(--text-color);
|
||||
padding: 8px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0px 12px 28px 0px,
|
||||
rgba(0, 0, 0, 0.1) 0px 2px 4px 0px,
|
||||
rgba(255, 255, 255, 0.05) 0px 0px 0px 1px inset;
|
||||
border-bottom-right-radius: $border-radius;
|
||||
border-bottom-left-radius: $border-radius;
|
||||
height: calc(100% - 41px);
|
||||
|
||||
code {
|
||||
color: var(--text-accent-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.terminal__banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
color: var(--text-color);
|
||||
|
||||
.terminal__author {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal__line {
|
||||
margin-bottom: 8px;
|
||||
|
||||
&::before {
|
||||
content: "Greg LEBRETON ~$ ";
|
||||
color: var(--username-color);
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
background: none;
|
||||
border: none;
|
||||
font-family: "Ubuntu Mono", monospace;
|
||||
color: var(--text-color);
|
||||
outline: none;
|
||||
font-size: 15px;
|
||||
width: calc(100% - 150px);
|
||||
}
|
||||
}
|
||||
|
||||
.terminal__response {
|
||||
margin: 8px 0 16px 0;
|
||||
|
||||
table {
|
||||
border: 1px dashed;
|
||||
padding: 4px;
|
||||
width: 100%;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: darkorange;
|
||||
}
|
||||
|
||||
thead {
|
||||
th {
|
||||
font-weight: normal;
|
||||
color: cadetblue;
|
||||
border-bottom: 1px solid white;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
td {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
tr:not(:last-child) {
|
||||
td {
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.socials {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
a {
|
||||
border-radius: 50%;
|
||||
background: var(--bg-2-social); /* fallback for old browsers */
|
||||
background: -webkit-linear-gradient(
|
||||
to left,
|
||||
var(--bg-1-social),
|
||||
var(--bg-2-social)
|
||||
); /* Chrome 10-25, Safari 5.1-6 */
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
var(--bg-1-social),
|
||||
var(--bg-2-social)
|
||||
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
|
||||
box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
background: var(--bg-2-social); /* fallback for old browsers */
|
||||
background: -webkit-linear-gradient(
|
||||
to right,
|
||||
var(--bg-1-social),
|
||||
var(--bg-2-social)
|
||||
); /* Chrome 10-25, Safari 5.1-6 */
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--bg-1-social),
|
||||
var(--bg-2-social)
|
||||
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
|
||||
box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
i {
|
||||
color: white;
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#banner-github {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 880px) {
|
||||
.terminal .terminal__body {
|
||||
.terminal__banner {
|
||||
pre {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
body {
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
position: unset;
|
||||
width: unset;
|
||||
height: unset;
|
||||
resize: none;
|
||||
.terminal__body {
|
||||
max-width: unset;
|
||||
width: 90vw;
|
||||
height: 70vh;
|
||||
|
||||
.terminal__banner {
|
||||
pre {
|
||||
font-size: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.socials {
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
bottom: unset;
|
||||
right: unset;
|
||||
}
|
||||
#banner-github {
|
||||
img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
#version {
|
||||
top: 38px;
|
||||
right: 38px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@import "snowflakes";
|
19
src/site.webmanifest
Normal file
19
src/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name":"Resume Greg Lebreton",
|
||||
"short_name":"Resume",
|
||||
"icons":[
|
||||
{
|
||||
"src":"./android-chrome-192x192.png",
|
||||
"sizes":"192x192",
|
||||
"type":"image/png"
|
||||
},
|
||||
{
|
||||
"src":"./android-chrome-512x512.png",
|
||||
"sizes":"512x512",
|
||||
"type":"image/png"
|
||||
}
|
||||
],
|
||||
"theme_color":"#ffffff",
|
||||
"background_color":"#ffffff",
|
||||
"display":"standalone"
|
||||
}
|
BIN
static/CV - Antoine DAUTRY.pdf
Normal file
BIN
static/CV - Antoine DAUTRY.pdf
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user