et un push ma foi!

main
greglebreton 2 years ago
commit bc3671ff60
  1. 16
      Dockerfile
  2. 21
      LICENCE
  3. 134
      README.md
  4. 5206
      package-lock.json
  5. 34
      package.json
  6. BIN
      src/android-chrome-192x192.png
  7. BIN
      src/android-chrome-512x512.png
  8. 248
      src/app.js
  9. BIN
      src/apple-touch-icon.png
  10. 67
      src/custom-comands.js
  11. 44
      src/draggable.js
  12. BIN
      src/favicon-16x16.png
  13. BIN
      src/favicon-32x32.png
  14. BIN
      src/favicon.ico
  15. 60
      src/index.html
  16. 87
      src/resources/commands.json
  17. 137
      src/scss/_snowflakes.scss
  18. 314
      src/scss/style.scss
  19. 19
      src/site.webmanifest
  20. BIN
      static/CV - Antoine DAUTRY.pdf

@ -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.

5206
package-lock.json generated

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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

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]
}' nest 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);
});

Binary file not shown.

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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

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…
Cancel
Save