commit
bc3671ff60
@ -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"] |
@ -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. |
@ -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. |
File diff suppressed because it is too large
Load Diff
@ -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" |
||||
} |
||||
} |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 33 KiB |
@ -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); |
||||
}); |
After Width: | Height: | Size: 8.9 KiB |
@ -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(); |
||||
} |
@ -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; |
||||
} |
||||
} |
After Width: | Height: | Size: 610 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 15 KiB |
@ -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> |
@ -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>" |
||||
] |
||||
] |
||||
} |
||||
] |
@ -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 |
||||
} |
@ -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"; |
@ -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" |
||||
} |
Binary file not shown.
Loading…
Reference in new issue