├── .gitignore
├── .parcelrc
├── Dockerfile
├── LICENCE
├── README.md
├── docker-compose.yml
├── package-lock.json
├── package.json
└── src
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── app.js
├── apple-touch-icon.png
├── custom-comands.js
├── draggable.js
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── images
├── halloween-bg.jpg
└── santa.gif
├── index.html
├── resources
└── commands.json
├── scss
├── _halloween.scss
├── _santa.scss
├── _snowflakes.scss
└── style.scss
├── site.webmanifest
├── sound
└── jingle-bells.mp3
└── utils.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .parcel-cache
3 | .idea
4 | dist
5 |
--------------------------------------------------------------------------------
/.parcelrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@parcel/config-default"]
3 | }
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:bullseye-slim as builder
2 | WORKDIR /data
3 | COPY . .
4 | RUN apt update && apt install -y npm
5 | RUN npm install -i package.json \
6 | && npm run build
7 |
8 | FROM alpine
9 |
10 | RUN apk update \
11 | && apk add lighttpd \
12 | && rm -rf /var/cache/apk/*
13 |
14 | COPY --from=builder /data/dist /var/www/localhost/htdocs
15 |
16 | CMD ["lighttpd","-D","-f","/etc/lighttpd/lighttpd.conf"]
17 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Antoine DAUTRY
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Resume Terminal
2 |
3 | ## About
4 |
5 | This projet use [ParcelJS](https://parceljs.org/) as build tool.
6 |
7 | It is made from scratch, some libraries are used for hidden commands :
8 |
9 | - `pif` [canvas-confetti](https://github.com/catdad/canvas-confetti).
10 | - `rm -rf /` [fireworks-js](https://github.com/crashmax-dev/fireworks-js/).
11 |
12 | ## Run the project
13 |
14 | > First you need to install dependencies with `npm install`
15 |
16 | - To run in dev mode : `npm run dev`
17 | - To build for production : `npm run build`
18 |
19 | ## Usage
20 |
21 | ### commands.json
22 |
23 | File `commands.json` contain all commands that just needs to display simple data and doesn't need a JS actions.
24 |
25 | For now, there are 4 possible type of steps :
26 |
27 | - list
28 | - text
29 | - code
30 | - table
31 |
32 | #### responseType = list
33 |
34 | To display a bullet list, the `value` field is an array of string.
35 |
36 | ```json
37 | {
38 | "command": "whois adautry",
39 | "responseType": "list",
40 | "value": [
41 | "A 27 years old full stack developper",
42 | "3 years of experiences",
43 | "Living in Nantes"
44 | ]
45 | }
46 | ```
47 |
48 | #### responseType = table
49 |
50 | Display a table, this object requires two fields :
51 |
52 | - `headers`: Headers of the array
53 | - `rows`: Array containing rows
54 |
55 | ```json
56 | {
57 | "command": "whereis experiences",
58 | "responseType": "table",
59 | "headers": [
60 | "Date",
61 | "Client",
62 | "Description",
63 | "Tech"
64 | ],
65 | "rows": [
66 | [
67 | "2021",
68 | "La Poste",
69 | "Internal tool to schedule techniciens on interventions.",
70 | "Angular 11, Spring Boot/Batch, Genetic algorithm"
71 | ],
72 | [
73 | "2020",
74 | "DSI",
75 | "Maintenance of a timesheet internal tool. Development of plugins for our ProjeQtor instance.",
76 | "Symfony, Angular 8"
77 | ]
78 | ]
79 | }
80 | ```
81 |
82 | #### responseType = text
83 |
84 | Just display text contained in `value`.
85 |
86 | ```json
87 | {
88 | "command": "find . -type f -print | xargs grep \"hobby\"",
89 | "responseType": "text",
90 | "value": "Bonsoir"
91 | }
92 | ```
93 |
94 | #### responseType = code
95 |
96 | Display code between `pre` tag, `value` is an array of string, each string is a line.
97 |
98 | ```json
99 | {
100 | "command": "curl https://adautry.fr/user/03101994",
101 | "responseType": "code",
102 | "value": [
103 | "{",
104 | " \"name\":\"Antoine DAUTRY\",",
105 | " \"job\":\"Fullstack developper\",",
106 | " \"experience\":\"3 years\",",
107 | " \"city\":\"Nantes\"",
108 | "}"
109 | ]
110 | }
111 | ```
112 |
113 | ## Customs commands
114 |
115 | In the `app.js` file you can see multiple arrays that stores commands :
116 |
117 | - `hiddenCommands`: Commands that are not use in autocompletion (easter egg commands for example)
118 | - `customCommands`: Commands that needs a specials JS treatments, in my case `dark`/`light` to swith app theme, `get cv`
119 | to download my resume, ...
120 | - `commandsList`: This is the main array used for autocompletion, it stores `customCommands` **and** commands that are
121 | listed in the `commands.json` file.
122 |
123 | ## Attributions
124 |
125 | - [Image from vector_corp](https://www.freepik.com/free-ai-image/halloween-scene-with-pumpkins-bats-full-moon_72868248.htm#query=haloween&position=4&from_view=search&track=sph&uuid=bedaf5ef-3c64-4822-82eb-3d4f750703f8)
126 | on Freepik
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 | services:
3 | resume-terminal:
4 | build:
5 | context: .
6 | dockerfile: Dockerfile
7 | # Not on DockerHub
8 | image: antoine1003/resume-terminal
9 | container_name: resume-terminal
10 | ports:
11 | - '80:80'
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cv-terminal",
3 | "version": "2.1.7",
4 | "description": "Nice looking resume.",
5 | "scripts": {
6 | "clean:output": "rimraf dist",
7 | "dev": "npx parcel src/index.html",
8 | "build": "npm run clean:output && npx parcel build src/index.html --no-source-maps"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "canvas-confetti": "^1.5.1",
15 | "dompurify": "^3.0.5",
16 | "fireworks-js": "^1.3.5",
17 | "postcss": "^8.3.11"
18 | },
19 | "browserslist": [
20 | "defaults"
21 | ],
22 | "devDependencies": {
23 | "@parcel/packager-raw-url": "^2.0.0",
24 | "@parcel/transformer-sass": "^2.0.0",
25 | "@parcel/transformer-webmanifest": "^2.0.0",
26 | "cssnano": "^5.0.8",
27 | "parcel": "^2.0.0",
28 | "prettier": "2.4.1",
29 | "rimraf": "^5.0.1",
30 | "sass": "^1.43.4"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoine1003/resume-terminal/7049af292e6573fb45b646f698a1915b6e6cd345/src/android-chrome-192x192.png
--------------------------------------------------------------------------------
/src/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoine1003/resume-terminal/7049af292e6573fb45b646f698a1915b6e6cd345/src/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef Command
3 | * @property {string} command
4 | * @property {string} responseType
5 | * @property {string?} value
6 | * @property {string[]?} headers
7 | * @property {string[]?} rows
8 | */
9 |
10 | /**
11 | * @type {Command[]} commands
12 | */
13 | import commands from "./resources/commands.json";
14 | import {
15 | getCV,
16 | pif,
17 | rmRf,
18 | setDarkMode,
19 | setHalloweenTheme,
20 | showSanta,
21 | showSantaAndRemoveListener
22 | } from "./custom-comands";
23 | import { stringToDom } from "./utils";
24 | import { dragElement } from "./draggable";
25 | import DOMPurify from 'dompurify';
26 |
27 | // Table containing the orders (useful for the completion of the orders)
28 | let commandsList = [];
29 | commands.forEach((c) => {
30 | commandsList.push(c.command);
31 | });
32 |
33 | // Commands that require JS processing
34 | const customCommands = ["clear", "dark", "light", "get cv"];
35 | commandsList = commandsList.concat(customCommands);
36 |
37 | // Eyster eggs' commands not available for autocompletion
38 | const hiddenCommands = ["pif", "rm -rf /", "hohoho", "boo"];
39 |
40 | // Added the ability to move the window for PCs
41 | if (window.innerWidth > 1024) {
42 | dragElement(document.querySelector(".terminal"));
43 | }
44 |
45 | // Order history table
46 | const commandsHistory = [];
47 | let historyMode = false;
48 | let historyIndex = -1;
49 | const terminalBody = document.querySelector(".terminal__body");
50 |
51 | // Adding the default line
52 | addNewLine();
53 |
54 | // December Easter egg, adding snowflakes
55 | const now = new Date();
56 | if (now.getMonth() === 11) {
57 | let htmlFlakes = "";
58 | for (let i = 0; i < 6; i++) {
59 | htmlFlakes += `
❅
❆
`;
60 | }
61 | const html = `${htmlFlakes}
`;
62 | document.body.append(stringToDom(html));
63 | }
64 |
65 | // Christmas Easter egg, adding Santa
66 | if (now.getMonth() === 11) {
67 | document.addEventListener('click', showSantaAndRemoveListener);
68 | }
69 |
70 |
71 | // Easter egg for Halloween, adding bats
72 | if (now.getMonth() === 9 && now.getDate() >= 28) {
73 | setHalloweenTheme();
74 | }
75 |
76 |
77 | // Set to dark mode if the browser theme is dark
78 | if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
79 | setDarkMode(true);
80 | }
81 |
82 | /**
83 | * Returns the HTML of the response for a given command
84 | * @param {string} command
85 | */
86 | function getDomForCommand(command) {
87 | const commandObj = commands.find((el) => el.command === command);
88 | let purifiedCommand = DOMPurify.sanitize(command);
89 | purifiedCommand = purifiedCommand.replace(//g, ">");
90 |
91 | console.log(purifiedCommand)
92 | let html = "";
93 | if (commandObj === undefined) {
94 | html = `'${
95 | purifiedCommand.split(" ")[0]
96 | }' is not recognized as an internal command or external command, operable program or batch file. Type the help
command to display a list of available commands.`;
97 | } else {
98 | if (commandObj.responseType === "list" && Array.isArray(commandObj.value)) {
99 | html = "";
100 | html += commandObj.value.map((s) => `${s} `).join("");
101 | html += " ";
102 | } else if (commandObj.responseType === "text") {
103 | html = commandObj.value;
104 | } else if (commandObj.responseType === "table") {
105 | const headers = commandObj.headers;
106 | const rows = commandObj.rows;
107 | const thsHtml = headers.map((h) => `${h} `).join("");
108 | const tdsHtml = rows
109 | .map((r) => `${r.map((rtd) => `${rtd} `).join("")} `)
110 | .join("");
111 | html = ``;
112 | } else if (commandObj.responseType === "code") {
113 | html = `${commandObj.value.join("\n")} `;
114 | }
115 | }
116 |
117 | return html;
118 | }
119 |
120 | /**
121 | * Adds a new command input line and disables the previous one.
122 | * @param {string|null} previousUid uid de la ligne précédente.
123 | */
124 | function addNewLine(previousUid = null) {
125 | const uid = Math.random().toString(36).replace("0.", "");
126 | // terminal__line
127 | const terminalLineEl = document.createElement("div");
128 | terminalLineEl.classList.add("terminal__line");
129 |
130 | // terminal__response
131 | const terminalResponseEl = document.createElement("div");
132 | terminalResponseEl.classList.add("terminal__response");
133 | terminalResponseEl.id = `response-${uid}`;
134 |
135 | // input text
136 | const inputEl = document.createElement("input");
137 | inputEl.type = "text";
138 | inputEl.id = `input-${uid}`;
139 | inputEl.autocapitalize = "off";
140 | inputEl.dataset.uid = uid;
141 | inputEl.dataset.active = "1"; // Needed for focus
142 | inputEl.addEventListener("keydown", onCommandInput);
143 |
144 | terminalLineEl.appendChild(inputEl);
145 | if (previousUid) {
146 | const previousInputEl = document.getElementById(previousUid);
147 | if (previousInputEl) {
148 | previousInputEl.setAttribute("disabled", "true");
149 | previousInputEl.removeEventListener("keydown", onCommandInput);
150 | delete previousInputEl.dataset.active;
151 | }
152 | }
153 | document.getElementById("terminal").appendChild(terminalLineEl);
154 | document.getElementById("terminal").appendChild(terminalResponseEl);
155 |
156 | inputEl.focus(); // Adds the focus as soon as the field is created
157 | }
158 |
159 | /**
160 | * Manages the keydown on the command input.
161 | * @param e
162 | */
163 | function onCommandInput(e) {
164 | const commandValue = e.target.value.trim().toLowerCase();
165 | if (e.keyCode === 13) {
166 | // ENTER
167 | if (commandValue !== "") {
168 | historyMode = false;
169 | const idResponse = `response-${e.target.dataset.uid}`;
170 | const responseEl = document.getElementById(idResponse);
171 | let html;
172 | if (
173 | hiddenCommands.includes(commandValue) ||
174 | customCommands.includes(commandValue)
175 | ) {
176 | html = handleCustomCommands(commandValue);
177 | } else {
178 | html = getDomForCommand(commandValue);
179 | }
180 | if (responseEl) {
181 | responseEl.innerHTML = html;
182 | commandsHistory.push(commandValue);
183 | addNewLine(e.target.id);
184 | }
185 | }
186 | } else if (e.keyCode === 9) {
187 | // TAB
188 | e.preventDefault();
189 | if (commandValue === "") {
190 | this.value = "help";
191 | } else {
192 | const matchingCommand = commandsList.find((c) =>
193 | c.startsWith(commandValue)
194 | );
195 | if (matchingCommand) {
196 | this.value = matchingCommand;
197 | }
198 | }
199 | historyMode = false;
200 | } else if (e.keyCode === 38 || e.keyCode === 40) {
201 | // UP / DOWN
202 | // History management
203 | if (commandsHistory.length > 0) {
204 | if (historyMode === false) {
205 | historyIndex = commandsHistory.length - 1;
206 | } else {
207 | if (e.keyCode === 38 && historyIndex !== 0) {
208 | // UP
209 | historyIndex--;
210 | } else if (
211 | e.keyCode === 40 &&
212 | historyIndex !== commandsHistory.length - 1
213 | ) {
214 | historyIndex++;
215 | }
216 | }
217 | this.value = commandsHistory[historyIndex];
218 | }
219 | historyMode = true;
220 | }
221 | }
222 |
223 | /**
224 | * Allows to manage hidden commands (not proposed in the autocompletion)
225 | * @param {string} command
226 | * @returns {string|void} Html to be displayed in the response of the command
227 | */
228 | function handleCustomCommands(command) {
229 | switch (command) {
230 | case "pif":
231 | pif();
232 | return "Let's go !";
233 | case "light":
234 | if (!document.body.classList.contains("dark-mode"))
235 | return "You are already in light mode.";
236 | setDarkMode(false);
237 | return "Your are now in light mode.";
238 | case "dark":
239 | if (document.body.classList.contains("dark-mode"))
240 | return "You are already in dark mode.";
241 | setDarkMode(true);
242 | return "You are now in dark mode.";
243 | case "get cv":
244 | getCV();
245 | return "The CV will be downloaded.";
246 | case "rm -rf /":
247 | rmRf();
248 | return "🎆";
249 | case "clear":
250 | terminalBody.innerHTML = `
`;
251 | return;
252 | case "boo":
253 | setHalloweenTheme();
254 | return "🎃";
255 | case "hohoho":
256 | showSanta();
257 | return "🎅🎁";
258 | }
259 | }
260 |
261 | // ------------------------------------------------------------------------------------
262 | // EVENT LISTENNER
263 | // ------------------------------------------------------------------------------------
264 |
265 | // Added focus on the input even if you click on the body (to keep the cursor)
266 | document.body.addEventListener("click", function (e) {
267 | if (e.target.tagName !== "INPUT") {
268 | const activeInput = document.querySelector("input[data-active]");
269 | activeInput.focus();
270 | }
271 | });
272 |
273 | document.querySelector(".fake-close").addEventListener("click", function (e) {
274 | const terminalEl = document.querySelector(".terminal");
275 | terminalEl.parentElement.removeChild(terminalEl);
276 | });
277 |
--------------------------------------------------------------------------------
/src/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoine1003/resume-terminal/7049af292e6573fb45b646f698a1915b6e6cd345/src/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/custom-comands.js:
--------------------------------------------------------------------------------
1 | import confetti from "canvas-confetti";
2 | import { Fireworks } from "fireworks-js";
3 | import { stringToDom } from "./utils";
4 |
5 | /**
6 | * Affiche des confettis sur la page
7 | */
8 | export function pif() {
9 | const count = 200;
10 | const defaults = {
11 | origin: { y: 0.7 },
12 | };
13 |
14 | function fire(particleRatio, opts) {
15 | confetti(
16 | Object.assign({}, defaults, opts, {
17 | particleCount: Math.floor(count * particleRatio),
18 | })
19 | );
20 | }
21 |
22 | fire(0.25, {
23 | spread: 26,
24 | startVelocity: 55,
25 | });
26 | fire(0.2, {
27 | spread: 60,
28 | });
29 | fire(0.35, {
30 | spread: 100,
31 | decay: 0.91,
32 | scalar: 0.8,
33 | });
34 | fire(0.1, {
35 | spread: 120,
36 | startVelocity: 25,
37 | decay: 0.92,
38 | scalar: 1.2,
39 | });
40 | fire(0.1, {
41 | spread: 120,
42 | startVelocity: 45,
43 | });
44 | }
45 |
46 | export function setDarkMode(value) {
47 | if (value) {
48 | document.body.classList.add("dark-mode");
49 | } else {
50 | document.body.classList.remove("dark-mode");
51 | }
52 | }
53 |
54 | export function getCV() {
55 | const a = document.createElement("a");
56 | a.href = "https://my-resume.adautry.fr/download-latest";
57 | a.setAttribute("download", "CV - Antoine DAUTRY.pdf");
58 | a.click();
59 | }
60 |
61 | export function rmRf() {
62 | if (document.body.classList.contains("firework")) return;
63 | setDarkMode(true);
64 | document.body.classList.add("firework");
65 | const fireworks = new Fireworks(document.body, {
66 | mouse: { click: true, move: false, max: 7 },
67 | });
68 | fireworks.start();
69 | }
70 |
71 | export function setHalloweenTheme() {
72 | const isActive = document.querySelector(".halloween-bg");
73 | if (isActive) return;
74 | // add image
75 | const imageUrl = new URL(
76 | 'images/halloween-bg.jpg',
77 | import.meta.url
78 | );
79 | const html = ` `;
80 | document.body.prepend(stringToDom(html));
81 | document.body.classList.add("halloween");
82 | setDarkMode(true);
83 | }
84 |
85 | /**
86 | * Shows Santa on the page and remove the listener
87 | * This function is needed to properly remove listener
88 | * with removeEventListener function
89 | */
90 | export function showSantaAndRemoveListener() {
91 | showSanta(true);
92 | }
93 |
94 | export function showSanta(removeOnClickListener = false) {
95 | if (removeOnClickListener) {
96 | document.removeEventListener('click', showSantaAndRemoveListener);
97 | }
98 | let santaEl = document.getElementById('santa');
99 |
100 | if (santaEl)
101 | return;
102 |
103 | const imageUrl = new URL(
104 | 'images/santa.gif',
105 | import.meta.url
106 | );
107 | const html = ` `
108 | document.body.prepend(stringToDom(html));
109 | santaEl = document.getElementById('santa')
110 |
111 | const santaOptions = {
112 | animationId: requestAnimationFrame(animateSanta),
113 | amountOfPixelsToAnimate: window.innerWidth + 200,
114 | duration: 5000,
115 | imageAngleCorrection: 6.0382, // In radian
116 | angleAtenuation: 4,
117 | topOffset: '5vh'
118 | }
119 |
120 | let right = 0;
121 | let startTime = null;
122 | const jingleBellsSoundUrl = new URL(
123 | 'sound/jingle-bells.mp3',
124 | import.meta.url
125 | );
126 |
127 | let jingleBellsSound = new Audio(jingleBellsSoundUrl);
128 | jingleBellsSound.play();
129 |
130 |
131 | function animateSanta(timestamp) {
132 | if (!startTime) {
133 | startTime = timestamp;
134 | }
135 |
136 | const runtime = timestamp - startTime;
137 | const relativeProgress = runtime / santaOptions.duration;
138 |
139 | right = santaOptions.amountOfPixelsToAnimate * Math.min(relativeProgress, 1);
140 |
141 | const { top, radian } = getAnimationData(relativeProgress);
142 |
143 | const angle = (radian + santaOptions.imageAngleCorrection);
144 |
145 | santaEl.style.transform = `translateX(-${right}px) translateY(calc(${santaOptions.topOffset} - ${top * 100}px)) rotate(${angle}rad)`;
146 |
147 | // We want to request another frame when our desired duration isn't met yet
148 | if (runtime < santaOptions.duration) {
149 | requestAnimationFrame(animateSanta);
150 | } else {
151 | santaEl.remove();
152 | jingleBellsSound.pause();
153 | cancelAnimationFrame(santaOptions.animationId);
154 | }
155 | }
156 |
157 | /**
158 | * Returns calculated fields needed for animation
159 | * @param {number} progress
160 | * @returns {top: number, radian: number}
161 | */
162 | function getAnimationData(progress) {
163 | progress = Math.max(0, Math.min(1, progress));
164 |
165 | // Calculate derivate and get the angle to calculate image rotation
166 | const derivate = -8 * progress + 4;
167 | const radian = Math.atan(derivate) / santaOptions.angleAtenuation
168 |
169 | return {
170 | top: 1 - 4 * (progress - 0.5) ** 2, // Parabol function
171 | radian
172 | };
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/src/draggable.js:
--------------------------------------------------------------------------------
1 | export function dragElement(elmnt) {
2 | var pos1 = 0,
3 | pos2 = 0,
4 | pos3 = 0,
5 | pos4 = 0;
6 | const element = document.querySelector(".terminal__header");
7 | if (element) {
8 | // if present, the header is where you move the DIV from:
9 | element.onmousedown = dragMouseDown;
10 | } else {
11 | // otherwise, move the DIV from anywhere inside the DIV:
12 | elmnt.onmousedown = dragMouseDown;
13 | }
14 |
15 | function dragMouseDown(e) {
16 | e = e || window.event;
17 | e.preventDefault();
18 | // get the mouse cursor position at startup:
19 | pos3 = e.clientX;
20 | pos4 = e.clientY;
21 | document.onmouseup = closeDragElement;
22 | // call a function whenever the cursor moves:
23 | document.onmousemove = elementDrag;
24 | }
25 |
26 | function elementDrag(e) {
27 | e = e || window.event;
28 | e.preventDefault();
29 | // calculate the new cursor position:
30 | pos1 = pos3 - e.clientX;
31 | pos2 = pos4 - e.clientY;
32 | pos3 = e.clientX;
33 | pos4 = e.clientY;
34 | // set the element's new position:
35 | elmnt.style.top = elmnt.offsetTop - pos2 + "px";
36 | elmnt.style.left = elmnt.offsetLeft - pos1 + "px";
37 | }
38 |
39 | function closeDragElement() {
40 | // stop moving when mouse button is released:
41 | document.onmouseup = null;
42 | document.onmousemove = null;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoine1003/resume-terminal/7049af292e6573fb45b646f698a1915b6e6cd345/src/favicon-16x16.png
--------------------------------------------------------------------------------
/src/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoine1003/resume-terminal/7049af292e6573fb45b646f698a1915b6e6cd345/src/favicon-32x32.png
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoine1003/resume-terminal/7049af292e6573fb45b646f698a1915b6e6cd345/src/favicon.ico
--------------------------------------------------------------------------------
/src/images/halloween-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoine1003/resume-terminal/7049af292e6573fb45b646f698a1915b6e6cd345/src/images/halloween-bg.jpg
--------------------------------------------------------------------------------
/src/images/santa.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoine1003/resume-terminal/7049af292e6573fb45b646f698a1915b6e6cd345/src/images/santa.gif
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | CV - Antoine DAUTRY
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
34 |
35 |
______ __ __ ______ ______ ______ __ __ __ __ __ ______ __
36 | /\ ___\ /\ \ / / /\__ _\ /\ ___\ /\ == \ /\ "-./ \ /\ \ /\ "-.\ \ /\ __ \ /\ \
37 | \ \ \____ \ \ \'/ \/_/\ \/ \ \ __\ \ \ __< \ \ \-./\ \ \ \ \ \ \ \-. \ \ \ __ \ \ \ \____
38 | \ \_____\ \ \__| \ \_\ \ \_____\ \ \_\ \_\ \ \_\ \ \_\ \ \_\ \ \_\\"\_\ \ \_\ \_\ \ \_____\
39 | \/_____/ \/_/ \/_/ \/_____/ \/_/ /_/ \/_/ \/_/ \/_/ \/_/ \/_/ \/_/\/_/ \/_____/
40 |
Antoine DAUTRY
41 |
Welcome to my CV! To view the available commands type help
. To validate each command press Enter , you can use the Tab key to help you complete a command.
42 |
43 |
44 |
45 |
46 |
47 |
52 |
53 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/src/resources/commands.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "command": "help",
4 | "responseType": "list",
5 | "value": [
6 | "about
: Displays information about me",
7 | "clear
: Clear terminal",
8 | "experiences
: Display the list of my professional experiences",
9 | "get cv
: Download CV",
10 | "help
: Display help",
11 | "hobbies
: Display the list of my hobbies",
12 | "projects
: Display the list of my personal projects",
13 | "dark/light
: Change theme",
14 | "You can use the TAB key to complete a command ",
15 | "You can find old commands with the up and down arrows. "
16 | ]
17 | },
18 | {
19 | "command": "about",
20 | "responseType": "code",
21 | "value": [
22 | "{",
23 | " \"name\" : \"Antoine DAUTRY\",",
24 | " \"poste\" : \"Fullstack developer\",",
25 | " \"experience\" : \"4 years\",",
26 | " \"city\" : \"Nantes, France\"",
27 | "}"
28 | ]
29 | },
30 | {
31 | "command": "experiences",
32 | "responseType": "table",
33 | "headers": [
34 | "Date",
35 | "Client",
36 | "Description",
37 | "Tech"
38 | ],
39 | "rows": [
40 | [
41 | "07/2023 Now",
42 | "Airbus Helicopters",
43 | "Setting up a website to provide tools for several internal departments",
44 | "Angular 16 Symfony 6 EventSource"
45 | ],
46 | [
47 | "11/2021 06/2023",
48 | "Elephant TechMinistry of Justice ",
49 | "Establishment of a platform linking the various actors in the reintegration of former prisoners.",
50 | "Angular 11 Spring Boot Axelor"
51 | ],
52 | [
53 | "03/2021 11/2021",
54 | "SIILa Poste ",
55 | "La Poste's internal tool for allocating technicians to different missions.",
56 | "Angular 11 Spring Boot Spring Batch"
57 | ],
58 | [
59 | "02/2020 03/2021",
60 | "SIII.T. Dept ",
61 | "Maintenance of an internal timesheet tool. Development of plugins for our version of ProjeQtor. Conversion of an old webapp to Angular 9.",
62 | "Symfony Angular 9"
63 | ],
64 | [
65 | "11/2019 02/2020",
66 | "SIIPoste IMMO ",
67 | "Work on a process management tool to manage the evolution of a work request.",
68 | "Symfony 3.4 AngularJS Processmaker"
69 | ]
70 | ]
71 | },
72 | {
73 | "command": "hobbies",
74 | "responseType": "list",
75 | "value": [
76 | "Music: Piano, Guitar",
77 | "Programmation: JS, Angular, PHP",
78 | "Other: Cinema, Aeronautics, Photography"
79 | ]
80 | },
81 | {
82 | "command": "projects",
83 | "responseType": "table",
84 | "headers": [
85 | "Name",
86 | "Description",
87 | "Tech",
88 | "Links"
89 | ],
90 | "rows": [
91 | [
92 | "Form to Resume - Web (2023)",
93 | "I set up a private web page where I can manually change data from my resume via a form. After validating changes, this will generate a PDF file that will be available on the public link.",
94 | "Angular 16, PHP 8.2",
95 | "Links "
96 | ],
97 | [
98 | "Chartsfinder - Web (2021)",
99 | "Web application to quickly find aeronautical maps. A C# version already existed but I preferred to update it with a web version which is easier to use.",
100 | "Angular 11, PHP 7.4",
101 | "Links "
102 | ],
103 | [
104 | "Personal website (2021)",
105 | "Personal website allowing me to show my projects and deploy a new version of the software. There is even a hidden game...",
106 | "Symfony 5",
107 | "Link "
108 | ],
109 | [
110 | "Chartsfinder - Software (2020)",
111 | "Software to quickly retrieve aeronautical charts.",
112 | "C# WPF",
113 | "Link "
114 | ]
115 | ]
116 | }
117 | ]
118 |
--------------------------------------------------------------------------------
/src/scss/_halloween.scss:
--------------------------------------------------------------------------------
1 | .halloween-bg {
2 | position: fixed;
3 | top: 0;
4 | height: 100vh;
5 | width: 100vw;
6 | overflow: hidden;
7 | object-fit: cover;
8 | object-position: top;
9 | opacity: .5;
10 | filter: saturate(1.5) brightness(1.2);
11 | }
12 |
13 | body.halloween .terminal .terminal__body {
14 | background: #00000036;
15 | box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
16 | backdrop-filter: blur(5px);
17 | }
18 |
19 | body:not(.dark-mode) .halloween-bg {
20 | opacity: .8;
21 | }
--------------------------------------------------------------------------------
/src/scss/_santa.scss:
--------------------------------------------------------------------------------
1 | #santa {
2 | width: 150px;
3 | position: absolute;
4 | top: 50px;
5 | right: -200px;
6 | z-index: 1;
7 | }
--------------------------------------------------------------------------------
/src/scss/_snowflakes.scss:
--------------------------------------------------------------------------------
1 | .snowflake {
2 | color: #fff;
3 | font-size: 1em;
4 | font-family: Arial, sans-serif;
5 | text-shadow: 0 0 5px #000;
6 | }
7 |
8 | @-webkit-keyframes snowflakes-fall {
9 | 0% {
10 | top: -10%
11 | }
12 | 100% {
13 | top: 100%
14 | }
15 | }
16 |
17 | @-webkit-keyframes snowflakes-shake {
18 | 0%, 100% {
19 | -webkit-transform: translateX(0);
20 | transform: translateX(0)
21 | }
22 | 50% {
23 | -webkit-transform: translateX(80px);
24 | transform: translateX(80px)
25 | }
26 | }
27 |
28 | @keyframes snowflakes-fall {
29 | 0% {
30 | top: -10%
31 | }
32 | 100% {
33 | top: 100%
34 | }
35 | }
36 |
37 | @keyframes snowflakes-shake {
38 | 0%, 100% {
39 | transform: translateX(0)
40 | }
41 | 50% {
42 | transform: translateX(80px)
43 | }
44 | }
45 |
46 | .snowflake {
47 | position: fixed;
48 | top: -10%;
49 | z-index: 9999;
50 | -webkit-user-select: none;
51 | -moz-user-select: none;
52 | -ms-user-select: none;
53 | user-select: none;
54 | cursor: default;
55 | -webkit-animation-name: snowflakes-fall, snowflakes-shake;
56 | -webkit-animation-duration: 10s, 3s;
57 | -webkit-animation-timing-function: linear, ease-in-out;
58 | -webkit-animation-iteration-count: infinite, infinite;
59 | -webkit-animation-play-state: running, running;
60 | animation-name: snowflakes-fall, snowflakes-shake;
61 | animation-duration: 10s, 3s;
62 | animation-timing-function: linear, ease-in-out;
63 | animation-iteration-count: infinite, infinite;
64 | animation-play-state: running, running
65 | }
66 |
67 | .snowflake:nth-of-type(0) {
68 | left: 1%;
69 | -webkit-animation-delay: 0s, 0s;
70 | animation-delay: 0s, 0s
71 | }
72 |
73 | .snowflake:nth-of-type(1) {
74 | left: 10%;
75 | -webkit-animation-delay: 1s, 1s;
76 | animation-delay: 1s, 1s
77 | }
78 |
79 | .snowflake:nth-of-type(2) {
80 | left: 20%;
81 | -webkit-animation-delay: 6s, .5s;
82 | animation-delay: 6s, .5s
83 | }
84 |
85 | .snowflake:nth-of-type(3) {
86 | left: 30%;
87 | -webkit-animation-delay: 4s, 2s;
88 | animation-delay: 4s, 2s
89 | }
90 |
91 | .snowflake:nth-of-type(4) {
92 | left: 40%;
93 | -webkit-animation-delay: 2s, 2s;
94 | animation-delay: 2s, 2s
95 | }
96 |
97 | .snowflake:nth-of-type(5) {
98 | left: 50%;
99 | -webkit-animation-delay: 8s, 3s;
100 | animation-delay: 8s, 3s
101 | }
102 |
103 | .snowflake:nth-of-type(6) {
104 | left: 60%;
105 | -webkit-animation-delay: 6s, 2s;
106 | animation-delay: 6s, 2s
107 | }
108 |
109 | .snowflake:nth-of-type(7) {
110 | left: 70%;
111 | -webkit-animation-delay: 2.5s, 1s;
112 | animation-delay: 2.5s, 1s
113 | }
114 |
115 | .snowflake:nth-of-type(8) {
116 | left: 80%;
117 | -webkit-animation-delay: 1s, 0s;
118 | animation-delay: 1s, 0s
119 | }
120 |
121 | .snowflake:nth-of-type(9) {
122 | left: 90%;
123 | -webkit-animation-delay: 3s, 1.5s;
124 | animation-delay: 3s, 1.5s
125 | }
126 |
127 | .snowflake:nth-of-type(10) {
128 | left: 25%;
129 | -webkit-animation-delay: 2s, 0s;
130 | animation-delay: 2s, 0s
131 | }
132 |
133 | .snowflake:nth-of-type(11) {
134 | left: 65%;
135 | -webkit-animation-delay: 4s, 2.5s;
136 | animation-delay: 4s, 2.5s
137 | }
138 |
--------------------------------------------------------------------------------
/src/scss/style.scss:
--------------------------------------------------------------------------------
1 | $border-radius: 5px;
2 |
3 | :root {
4 | --text-color: #fff;
5 | --text-accent-color: darksalmon;
6 | --link-color: darkorange;
7 | --bg-1: #f27121;
8 | --bg-2: #e94057;
9 | --bg-3: #8a2387;
10 | --bg-1-social: #f3a183;
11 | --bg-2-social: #ec6f66;
12 | --username-color: cadetblue;
13 | --terminal-bg: rgba(56, 4, 40, 0.9);
14 | --terminal-header-bg: #bbb;
15 | }
16 |
17 | body {
18 | overflow: hidden;
19 |
20 | &.dark-mode {
21 | --text-accent-color: #ffca85;
22 | --link-color: burlywood;
23 | --bg-1: #211F20;
24 | --bg-2: #292D34;
25 | --bg-3: #213030;
26 | --bg-1-social: #414141;
27 | --bg-2-social: #485461;
28 | --username-color: #858585;
29 | --terminal-bg: rgb(0 0 0 / 90%);
30 | --terminal-header-bg: #585252;
31 |
32 | &.firework {
33 | --terminal-bg: rgb(0 0 0 / 15%);
34 | }
35 | }
36 |
37 | box-sizing: border-box;
38 | margin: 0;
39 | display: flex;
40 | justify-content: space-around;
41 | align-items: center;
42 | flex-direction: column;
43 | height: 100vh;
44 | background: var(--bg-3); /* fallback for old browsers */
45 | background: -webkit-linear-gradient(
46 | to right,
47 | var(--bg-1),
48 | var(--bg-2),
49 | var(--bg-3)
50 | ); /* Chrome 10-25, Safari 5.1-6 */
51 | background: linear-gradient(
52 | to right,
53 | var(--bg-1),
54 | var(--bg-2),
55 | var(--bg-3)
56 | ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
57 | }
58 |
59 | ul {
60 | margin: 0;
61 | }
62 |
63 | .terminal {
64 | position: absolute;
65 | resize: both;
66 | overflow: hidden;
67 | height: 450px;
68 | width: min(900px, 90vw);
69 |
70 | &::-webkit-resizer {
71 | background: transparent;
72 | }
73 |
74 | .terminal__header {
75 | height: 25px;
76 | padding: 0 8px;
77 | background-color: var(--terminal-header-bg);
78 | margin: 0 auto;
79 | border-top-right-radius: $border-radius;
80 | border-top-left-radius: $border-radius;
81 | cursor: move;
82 |
83 | .fake-button {
84 | height: 10px;
85 | width: 10px;
86 | border-radius: 50%;
87 | border: 1px solid #000;
88 | position: relative;
89 | top: 6px;
90 | left: 6px;
91 | display: inline-block;
92 | cursor: pointer;
93 |
94 | &.fake-close {
95 | left: 6px;
96 | background-color: #ff3b47;
97 | border-color: #9d252b;
98 | }
99 |
100 | &.fake-minimize {
101 | left: 11px;
102 | background-color: #ffc100;
103 | border-color: #9d802c;
104 | }
105 |
106 | &.fake-zoom {
107 | left: 16px;
108 | background-color: #00d742;
109 | border-color: #049931;
110 | }
111 | }
112 | }
113 |
114 | .terminal__body {
115 | font-family: "Ubuntu Mono", monospace;
116 | background: var(--terminal-bg);
117 | color: var(--text-color);
118 | padding: 8px;
119 | overflow-y: scroll;
120 | overflow-x: hidden;
121 | box-shadow: rgba(0, 0, 0, 0.2) 0px 12px 28px 0px,
122 | rgba(0, 0, 0, 0.1) 0px 2px 4px 0px,
123 | rgba(255, 255, 255, 0.05) 0px 0px 0px 1px inset;
124 | border-bottom-right-radius: $border-radius;
125 | border-bottom-left-radius: $border-radius;
126 | height: calc(100% - 41px);
127 |
128 | /* width */
129 | &::-webkit-scrollbar {
130 | width: 5px;
131 | }
132 |
133 | /* Track */
134 | &::-webkit-scrollbar-track {
135 | background: transparent;
136 | }
137 |
138 | /* Handle */
139 | &::-webkit-scrollbar-thumb {
140 | background: var(--text-accent-color);
141 | }
142 |
143 | /* Handle on hover */
144 | &::-webkit-scrollbar-thumb:hover {
145 | background: var(--text-accent-color);
146 | }
147 |
148 | code {
149 | color: var(--text-accent-color);
150 | font-size: 14px;
151 | }
152 |
153 | .terminal__banner {
154 | display: flex;
155 | flex-direction: column;
156 | justify-content: center;
157 | color: var(--text-color);
158 |
159 | .terminal__author {
160 | text-align: right;
161 | }
162 | }
163 |
164 | .terminal__line {
165 | margin-bottom: 8px;
166 |
167 | &::before {
168 | content: "Antoine DAUTRY ~$ ";
169 | color: var(--username-color);
170 | }
171 |
172 | input[type="text"] {
173 | background: none;
174 | border: none;
175 | font-family: "Ubuntu Mono", monospace;
176 | color: var(--text-color);
177 | outline: none;
178 | font-size: 15px;
179 | width: calc(100% - 150px);
180 | }
181 | }
182 |
183 | .terminal__response {
184 | margin: 8px 0 16px 0;
185 |
186 | table {
187 | border: 1px dashed;
188 | padding: 4px;
189 | width: 100%;
190 |
191 | a {
192 | text-decoration: none;
193 | color: darkorange;
194 | }
195 |
196 | thead {
197 | th {
198 | font-weight: normal;
199 | color: cadetblue;
200 | border-bottom: 1px solid white;
201 | padding-bottom: 4px;
202 | }
203 | }
204 |
205 | tbody {
206 | td {
207 | padding: 4px;
208 | }
209 |
210 | tr:not(:last-child) {
211 | td {
212 | border-bottom: 1px solid white;
213 | }
214 | }
215 | }
216 | }
217 | }
218 | }
219 | }
220 |
221 | .socials {
222 | position: absolute;
223 | right: 16px;
224 | bottom: 16px;
225 | display: flex;
226 | gap: 16px;
227 |
228 | a {
229 | border-radius: 50%;
230 | background: var(--bg-2-social); /* fallback for old browsers */
231 | background: -webkit-linear-gradient(
232 | to left,
233 | var(--bg-1-social),
234 | var(--bg-2-social)
235 | ); /* Chrome 10-25, Safari 5.1-6 */
236 | background: linear-gradient(
237 | to left,
238 | var(--bg-1-social),
239 | var(--bg-2-social)
240 | ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
241 | box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
242 | width: 4em;
243 | height: 4em;
244 | display: flex;
245 | justify-content: center;
246 | align-items: center;
247 | text-decoration: none;
248 |
249 | &:hover {
250 | background: var(--bg-2-social); /* fallback for old browsers */
251 | background: -webkit-linear-gradient(
252 | to right,
253 | var(--bg-1-social),
254 | var(--bg-2-social)
255 | ); /* Chrome 10-25, Safari 5.1-6 */
256 | background: linear-gradient(
257 | to right,
258 | var(--bg-1-social),
259 | var(--bg-2-social)
260 | ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
261 | box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
262 | width: 4em;
263 | height: 4em;
264 | display: flex;
265 | justify-content: center;
266 | align-items: center;
267 | text-decoration: none;
268 | }
269 |
270 | i {
271 | color: white;
272 | font-size: 2em;
273 | }
274 | }
275 | }
276 |
277 | #banner-github {
278 | position: absolute;
279 | top: 0;
280 | right: 0;
281 | }
282 |
283 | @media (max-width: 880px) {
284 | .terminal .terminal__body {
285 | .terminal__banner {
286 | pre {
287 | font-size: 10px;
288 | }
289 | }
290 | }
291 | }
292 |
293 | @media (max-width: 640px) {
294 | body {
295 | align-items: center;
296 | flex-direction: column;
297 | justify-content: space-evenly;
298 | }
299 | canvas {
300 | position: fixed;
301 | top: 0;
302 | bottom: 0;
303 | left: 0;
304 | right: 0;
305 | z-index: -1;
306 | }
307 |
308 | .terminal {
309 | position: unset;
310 | width: unset;
311 | height: unset;
312 | resize: none;
313 | z-index: 2;
314 |
315 | .terminal__body {
316 | max-width: unset;
317 | width: 90vw;
318 | height: 70vh;
319 |
320 | .terminal__banner {
321 | pre {
322 | font-size: 5px;
323 | }
324 | }
325 | }
326 | }
327 | .socials {
328 | font-size: 13px;
329 | position: relative;
330 | bottom: unset;
331 | right: unset;
332 | }
333 | #banner-github {
334 | img {
335 | width: 100px;
336 | height: 100px;
337 | }
338 | }
339 | #version {
340 | top: 38px;
341 | right: 38px;
342 | font-size: 13px;
343 | }
344 | }
345 |
346 | @import "snowflakes";
347 | @import "halloween";
348 | @import "santa";
349 |
--------------------------------------------------------------------------------
/src/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name":"Resume Antoine DAUTRY",
3 | "short_name":"Resume",
4 | "icons":[
5 | {
6 | "src":"./android-chrome-192x192.png",
7 | "sizes":"192x192",
8 | "type":"image/png"
9 | },
10 | {
11 | "src":"./android-chrome-512x512.png",
12 | "sizes":"512x512",
13 | "type":"image/png"
14 | }
15 | ],
16 | "theme_color":"#ffffff",
17 | "background_color":"#ffffff",
18 | "display":"standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/src/sound/jingle-bells.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoine1003/resume-terminal/7049af292e6573fb45b646f698a1915b6e6cd345/src/sound/jingle-bells.mp3
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Convert HTML to DOM object
3 | * @param html
4 | * @returns {DocumentFragment}
5 | */
6 | export function stringToDom(html) {
7 | return document.createRange().createContextualFragment(html);
8 | }
--------------------------------------------------------------------------------