aboutsummaryrefslogtreecommitdiffstats
path: root/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'src/js')
-rw-r--r--src/js/app.js382
-rw-r--r--src/js/config/projects.js224
-rw-r--r--src/js/i18n/i18n.js42
-rw-r--r--src/js/i18n/locales/en.js36
-rw-r--r--src/js/i18n/locales/fr.js36
-rw-r--r--src/js/utilities/animations.js45
-rw-r--r--src/js/utilities/helpers.js19
7 files changed, 784 insertions, 0 deletions
diff --git a/src/js/app.js b/src/js/app.js
new file mode 100644
index 0000000..38aee0e
--- /dev/null
+++ b/src/js/app.js
@@ -0,0 +1,382 @@
+import projects from './config/projects';
+import {
+ translate,
+ setLocale,
+ currentLocale,
+ supportedLanguages,
+} from './i18n/i18n';
+import {
+ hideToBottom,
+ hideToLeft,
+ showFromBottom,
+ showFromLeft,
+} from './utilities/animations';
+import { isSmallVw, isStyleJsExists } from './utilities/helpers';
+
+/**
+ * Show/hide header and footer with slide animation (left).
+ */
+function toggleHeaderFooter() {
+ const header = document.querySelector('header');
+ const footer = document.querySelector('footer');
+ const elements = [header, footer];
+
+ elements.forEach((el) => {
+ if (el.classList.contains('hide')) {
+ showFromLeft(el);
+ } else {
+ hideToLeft(el);
+ }
+ });
+}
+
+/**
+ * Show/hide project details with slide animation (bottom).
+ */
+function toggleProjectDetails() {
+ const details = document.querySelector('.project-details');
+
+ if (details.classList.contains('hide')) {
+ showFromBottom(details);
+ } else {
+ hideToBottom(details);
+ }
+}
+
+/**
+ * Add an event listener to show or hide header and footer.
+ */
+function listenMenuBtn() {
+ const menuBtn = document.querySelector('.btn--menu');
+ menuBtn.addEventListener('click', toggleHeaderFooter);
+}
+
+/**
+ * Show or hide the project details button depending on current location.
+ */
+function toggleProjectDetailsBtn() {
+ const button = document.querySelector('.btn--details');
+ const currentPath = window.location.hash;
+
+ if (currentPath) {
+ button.style.display = '';
+ } else {
+ button.style.display = 'none';
+ }
+}
+
+/**
+ * Update the visibility of some DOM elements depending on viewport.
+ */
+function updateView() {
+ const header = document.querySelector('header');
+ const footer = document.querySelector('footer');
+ const toolbar = document.querySelector('.toolbar');
+ const details = document.querySelector('.project-details');
+
+ if (isSmallVw()) {
+ header.classList.add('hide');
+ footer.classList.add('hide');
+ toolbar.classList.remove('hide');
+ details?.classList.add('hide');
+ details?.classList.remove('fade-in');
+ } else {
+ showFromLeft(header);
+ showFromLeft(footer);
+ toolbar.classList.add('hide');
+ details?.classList.remove('hide');
+ details?.classList.add('fade-in');
+ }
+
+ toggleProjectDetailsBtn();
+}
+
+/**
+ * Update view when the window size changes.
+ */
+function listenWindowSize() {
+ window.addEventListener('resize', updateView);
+}
+
+/**
+ * Retrieve a project by id.
+ * @param {Integer} id - The project id.
+ * @returns {Object} The current project.
+ */
+function getCurrentProject(id) {
+ return projects.find((project) => project.id === id);
+}
+
+/**
+ * Get a list item for the given repo.
+ * @param {String} name - The repository name.
+ * @param {String} url - The repository URL.
+ * @returns {HTMLElement} A list item.
+ */
+function getRepoItem(name, url) {
+ const item = document.createElement('li');
+ const link = document.createElement('a');
+ const span = document.createElement('span');
+ span.classList.add('screen-reader-text');
+ span.textContent = name;
+ link.classList.add('list__link', `list__link--${name.toLocaleLowerCase()}`);
+ link.href = url;
+ link.appendChild(span);
+ item.classList.add('list__item');
+ item.appendChild(link);
+ return item;
+}
+
+/**
+ * Get the repos list wrapped inside ul element and the title.
+ * @param {Object[]} repos - An array of repo with name and URL.
+ * @returns {[title, list]} An array of HTMLElements for title and list.
+ */
+function getRepos(repos) {
+ if (repos.length === 0) return [];
+
+ const wrapper = document.createElement('div');
+ const title = document.createElement('h3');
+ const list = document.createElement('ul');
+ const items = repos.map((repo) => getRepoItem(repo.name, repo.url));
+ title.classList.add('project-details__title');
+ title.textContent = translate('main.project.details.repo', {
+ count: repos.length,
+ });
+ list.classList.add('list', 'list--repos');
+ list.append(...items);
+ wrapper.append(title, list);
+ return [title, list];
+}
+
+/**
+ * Get the technologies list wrapped inside ul element and the title.
+ * @param {String[]} technologies - An array of technology names.
+ * @returns {[title, list]} An array of HTMLElements for title and list.
+ */
+function getTechs(technologies) {
+ if (technologies.length === 0) return [];
+
+ const title = document.createElement('h3');
+ title.classList.add('project-details__title');
+ title.textContent = translate('main.project.details.tech', {
+ count: technologies.length,
+ });
+ const list = document.createElement('ul');
+ const items = technologies.map((technology) => {
+ const item = document.createElement('li');
+ item.textContent = technology;
+ return item;
+ });
+ list.classList.add('list', 'list--tech');
+ list.append(...items);
+ return [title, list];
+}
+
+/**
+ * Retrieve the project details.
+ * @param {Object} project - The project.
+ * @returns {HTMLElement} The project details wrapped in a div.
+ */
+function getProjectDetails(project) {
+ const details = document.createElement('div');
+ const title = document.createElement('h2');
+ const techList = project?.technologies ? getTechs(project.technologies) : [];
+ const reposList = getRepos(project.repo);
+ const locale = currentLocale();
+ let description;
+
+ if (project.description) {
+ description = document.createElement('div');
+ description.classList.add('project-details__description');
+ description.textContent = project.description[locale] || '';
+ } else {
+ description = '';
+ }
+
+ title.classList.add('project-details__title');
+ title.textContent = translate('main.project.details.about', {
+ name: project.name,
+ });
+ details.classList.add('project-details');
+ if (!isSmallVw()) details.classList.add('fade-in');
+ details.replaceChildren(title, description, ...techList, ...reposList);
+
+ return details;
+}
+
+/**
+ * Get an iframe for the given path/url.
+ * @param {String} src - The path/url to use as source.
+ * @returns {HTMLElement} The iframe.
+ */
+function getIframe(src) {
+ const iframe = document.createElement('iframe');
+ iframe.src = src;
+ return iframe;
+}
+
+/**
+ * Retrieve the project preview.
+ * @param {String} projectPath - The project path.
+ * @returns {HTMLElement} The project preview wrapped in a div.
+ */
+function getProjectPreview(projectPath) {
+ const preview = document.createElement('div');
+ const iframe = getIframe(projectPath);
+ preview.classList.add('project-preview', 'fade-in');
+ preview.replaceChildren(iframe);
+ return preview;
+}
+
+/**
+ * Display the targeted project.
+ * @param {String} id - The project id.
+ */
+function showProject(id) {
+ const currentProject = getCurrentProject(id);
+ const main = document.querySelector('.main');
+ const details = getProjectDetails(currentProject);
+ const preview = getProjectPreview(currentProject.path);
+ const detailsBtn = document.querySelector('.btn--details');
+
+ if (isSmallVw()) details.classList.add('hide');
+
+ detailsBtn.textContent = translate('main.project.details.about', {
+ name: currentProject.name,
+ });
+ detailsBtn.addEventListener('click', toggleProjectDetails);
+ window.history.pushState({}, currentProject.name, `/#${id}`);
+ document.title = `${currentProject.name} | Demo | Armand Philippot`;
+ main.replaceChildren(preview, details);
+}
+
+/**
+ * Add a CSS class to the current project in projects nav.
+ * @param {String} id - The project id.
+ */
+function setSelectedProject(id) {
+ const links = document.querySelectorAll('.nav__link');
+ links.forEach((link) => {
+ if (link.id === id) {
+ link.classList.add('nav__link--selected');
+ } else {
+ link.classList.remove('nav__link--selected');
+ }
+ });
+}
+
+/**
+ * Create a list item for a project.
+ * @param {String} id - The project id.
+ * @param {String} name - The project name.
+ * @returns {HTMLElement} The list item.
+ */
+function getProjectsNavItem(id, name) {
+ const item = document.createElement('li');
+ const link = document.createElement('a');
+ link.classList.add('nav__link');
+ link.href = `/#${id}`;
+ link.id = id;
+ link.textContent = name;
+ link.addEventListener('click', (e) => {
+ e.preventDefault();
+ showProject(id);
+ setSelectedProject(id);
+ toggleProjectDetailsBtn();
+ if (isSmallVw()) toggleHeaderFooter();
+ });
+ item.classList.add('nav__item');
+ item.appendChild(link);
+ return item;
+}
+
+/**
+ * Print the list of available projects.
+ */
+function printProjectsNav() {
+ const ul = document.querySelector('.nav .nav__list');
+
+ projects.forEach((project) => {
+ const item = getProjectsNavItem(project.id, project.name);
+ ul.appendChild(item);
+ });
+}
+
+/**
+ * Add style.js script for development purposes.
+ */
+function loadWebpackStyles() {
+ if (isStyleJsExists()) {
+ const head = document.querySelector('head');
+ const script = document.createElement('script');
+ script.src = 'assets/js/style.js';
+ head.appendChild(script);
+ }
+}
+
+/**
+ * Load corresponding project if the requested page contains a hash.
+ */
+function printRequestedPage() {
+ const currentPath = window.location.hash;
+
+ if (currentPath) {
+ const id = currentPath.replace('#', '');
+ showProject(id);
+ setSelectedProject(id);
+ }
+}
+
+/**
+ * Replace the legal notice link and text.
+ */
+function replaceLegalNoticeLink() {
+ const link = document.querySelector('.nav__link--legal');
+ link.href = translate('footer.legalNotice.link');
+ link.textContent = translate('footer.legalNotice.txt');
+}
+
+/**
+ * Translate all text available in HTML templates.
+ */
+function translateHTMLContent() {
+ const brandingDesc = document.querySelector('.branding__description');
+ const navLabel = document.querySelector('.nav__label');
+ const license = document.querySelector('.copyright__license');
+ const instructions = document.querySelector('.instructions');
+ brandingDesc.textContent = translate('branding.description');
+ navLabel.textContent = translate('nav.title');
+ license.title = translate('footer.license');
+ if (instructions) instructions.textContent = translate('main.instructions');
+}
+
+/**
+ * Translate the website according to the user preferred language.
+ */
+function setAppLocale() {
+ const preferredLanguage = navigator.language;
+ const supportedLanguage = supportedLanguages.find(
+ (lang) => preferredLanguage.startsWith(lang.code)
+ // eslint-disable-next-line function-paren-newline -- Conflict with Prettier
+ );
+ const locale = supportedLanguage?.code || 'en';
+ setLocale(locale);
+}
+
+/**
+ * Initialize the website with the projects list.
+ */
+function init() {
+ setAppLocale();
+ translateHTMLContent();
+ replaceLegalNoticeLink();
+ loadWebpackStyles();
+ printProjectsNav();
+ updateView();
+ listenWindowSize();
+ listenMenuBtn();
+ printRequestedPage();
+}
+
+init();
diff --git a/src/js/config/projects.js b/src/js/config/projects.js
new file mode 100644
index 0000000..53f1af8
--- /dev/null
+++ b/src/js/config/projects.js
@@ -0,0 +1,224 @@
+const projects = [
+ {
+ id: 'bin2dec',
+ name: 'Bin2Dec',
+ description: {
+ en: 'Convert a binary string to a decimal number.',
+ fr: 'Convertit un nombre binaire en un nombre décimal.',
+ },
+ path: './projects/js-small-apps/bin2dec/index.html',
+ repo: [
+ {
+ name: 'Github',
+ url: 'https://github.com/ArmandPhilippot/js-small-apps/tree/main/bin2dec',
+ },
+ {
+ name: 'Gitlab',
+ url: 'https://gitlab.com/ArmandPhilippot/js-small-apps/-/tree/main/bin2dec',
+ },
+ ],
+ technologies: ['Vanilla Javascript'],
+ },
+ {
+ id: 'budget-app',
+ name: 'Budget App',
+ description: {
+ en: 'By selecting a language in the initialization form, only the currency is converted (the app is not translated). Also, no data is saved on page reload.',
+ fr: "En sélectionnant une langue dans le formulaire d'initialisation, seul le format des nombres change (l'application n'est pas traduite). Aucune donnée n'est conservée après rechargement de la page.",
+ },
+ path: './projects/js-small-apps/budget-app/index.html',
+ repo: [
+ {
+ name: 'Github',
+ url: 'https://github.com/ArmandPhilippot/js-small-apps/tree/main/budget-app',
+ },
+ {
+ name: 'Gitlab',
+ url: 'https://gitlab.com/ArmandPhilippot/js-small-apps/-/tree/main/budget-app',
+ },
+ ],
+ technologies: ['Vanilla Javascript'],
+ },
+ {
+ id: 'calculator',
+ name: 'Calculator',
+ description: {
+ en: 'A basic calculator. Decimal part is limited to 3 digits. The first part is limited to 8 digits. If the result does not respect these limits, you will see an error.',
+ fr: 'Une simple calculette. La partie décimale est limitée à 3 chiffres. La première partie est limitée à 8 chiffres. Si le résultat ne respecte pas ces limites, vous verrez une erreur.',
+ },
+ path: './projects/js-small-apps/calculator/index.html',
+ repo: [
+ {
+ name: 'Github',
+ url: 'https://github.com/ArmandPhilippot/js-small-apps/tree/main/calculator',
+ },
+ {
+ name: 'Gitlab',
+ url: 'https://gitlab.com/ArmandPhilippot/js-small-apps/-/tree/main/calculator',
+ },
+ ],
+ technologies: ['Vanilla Javascript'],
+ },
+ {
+ id: 'clock',
+ name: 'Clock',
+ description: {
+ en: 'What time is it? You can have the current hour in three formats: an analogic clock, a numeric display or a text.',
+ fr: "Quelle heure est-il ? Vous pouvez voir l'heure actuelle dans trois formats différents : une horloge analogique, un affichage numérique et sous forme de texte.",
+ },
+ path: './projects/js-small-apps/clock/index.html',
+ repo: [
+ {
+ name: 'Github',
+ url: 'https://github.com/ArmandPhilippot/js-small-apps/tree/main/clock',
+ },
+ {
+ name: 'Gitlab',
+ url: 'https://gitlab.com/ArmandPhilippot/js-small-apps/-/tree/main/clock',
+ },
+ ],
+ technologies: ['Vanilla Javascript', 'SVG'],
+ },
+ {
+ id: 'color-cycle',
+ name: 'Color cycle',
+ description: {
+ en: 'Play with hexadecimal colors. Set a color, then choose one or more increment values and start the preview.',
+ fr: "Jouez avec les couleurs hexadécimales. Définissez une couleur, puis choisissez une ou plusieurs valeurs d'incrémentation et démarrez l'aperçu.",
+ },
+ path: './projects/js-small-apps/color-cycle/index.html',
+ repo: [
+ {
+ name: 'Github',
+ url: 'https://github.com/ArmandPhilippot/js-small-apps/tree/main/color-cycle',
+ },
+ {
+ name: 'Gitlab',
+ url: 'https://gitlab.com/ArmandPhilippot/js-small-apps/-/tree/main/color-cycle',
+ },
+ ],
+ technologies: ['Vanilla Javascript'],
+ },
+ {
+ id: 'css-border-previewer',
+ name: 'CSS Border Previewer',
+ description: {
+ en: 'Play with CSS borders (style, width, radius). Then, you can copy the generated code if the preview suits you.',
+ fr: "Jouez avec les bordures CSS (style, largeur, radius). Ensuite, vous pouvez copier le code généré si l'aperçu vous satisfait.",
+ },
+ path: './projects/js-small-apps/css-border-previewer/index.html',
+ repo: [
+ {
+ name: 'Github',
+ url: 'https://github.com/ArmandPhilippot/js-small-apps/tree/main/css-border-previewer',
+ },
+ {
+ name: 'Gitlab',
+ url: 'https://gitlab.com/ArmandPhilippot/js-small-apps/-/tree/main/css-border-previewer',
+ },
+ ],
+ technologies: ['Vanilla Javascript'],
+ },
+ {
+ id: 'meme-generator',
+ name: 'Meme Generator',
+ description: {
+ en: 'Choose a random image, set one or more texts then position them. Your meme is ready!',
+ fr: 'Choisissez une image aléatoire, définissez un ou plusieurs textes et positionnez-les. Votre meme est prêt !',
+ },
+ path: './projects/react-small-apps/meme-generator/build/index.html',
+ repo: [
+ {
+ name: 'Github',
+ url: 'https://github.com/ArmandPhilippot/react-small-apps/tree/main/meme-generator',
+ },
+ {
+ name: 'Gitlab',
+ url: 'https://gitlab.com/ArmandPhilippot/react-small-apps/-/tree/main/meme-generator',
+ },
+ ],
+ technologies: ['React', 'Fetch'],
+ },
+ {
+ id: 'notebook',
+ name: 'Notebook',
+ description: {
+ en: 'Create as many pages as you want and fill them. You can define a title and a body. Then you can easily navigate between your pages with the nav.',
+ fr: 'Créez autant de pages que vous le souhaitez et remplissez-les. Vous pouvez définir un titre et un corps de texte. Ensuite, vous pouvez facilement naviguer entre vos pages grâce à la navigation.',
+ },
+ path: './projects/react-small-apps/notebook/build/',
+ repo: [
+ {
+ name: 'Github',
+ url: 'https://github.com/ArmandPhilippot/react-small-apps/tree/main/notebook',
+ },
+ {
+ name: 'Gitlab',
+ url: 'https://gitlab.com/ArmandPhilippot/react-small-apps/-/tree/main/notebook',
+ },
+ ],
+ technologies: ['React', 'React router'],
+ },
+ {
+ id: 'rps-game',
+ name: 'Rock Paper Scissors',
+ description: {
+ en: 'A basic implementation of the game. Try to beat your friend or the computer.',
+ fr: "Une implémentation du jeu. Essayez de battre votre ami ou l'ordinateur.",
+ },
+ path: './projects/js-small-apps/rock-paper-scissors/index.html',
+ repo: [
+ {
+ name: 'Github',
+ url: 'https://github.com/ArmandPhilippot/js-small-apps/tree/main/rock-paper-scissors',
+ },
+ {
+ name: 'Gitlab',
+ url: 'https://gitlab.com/ArmandPhilippot/js-small-apps/-/tree/main/rock-paper-scissors',
+ },
+ ],
+ technologies: ['Vanilla Javascript'],
+ },
+ {
+ id: 'todos',
+ name: 'Todos',
+ description: {
+ en: 'You can add, remove or mark as done your todos. For each todos, you can add some details in addition to the title.\n\nLogin: demo@email.com\nPassword: demo',
+ fr: 'Vous pouvez ajouter, supprimer ou marquer comme fait vos "todo". Pour chaque "todo", vous pouvez ajouter des détails en plus du titre.\n\nLogin : demo@email.com\nMot de passe : demo',
+ },
+ path: './projects/react-small-apps/todos/build/',
+ repo: [
+ {
+ name: 'Github',
+ url: 'https://github.com/ArmandPhilippot/react-small-apps/tree/main/todos',
+ },
+ {
+ name: 'Gitlab',
+ url: 'https://gitlab.com/ArmandPhilippot/react-small-apps/-/tree/main/todos',
+ },
+ ],
+ technologies: ['React', 'React router', 'Redux'],
+ },
+ {
+ id: 'users-list',
+ name: 'Users list',
+ description: {
+ en: 'You can see a list of username. By clicking on it, the next column display information about the selected user.',
+ fr: "Vous pouvez voir une liste de noms d'utilisateur. En cliquant sur l'un d'eux, la colonne suivante affiche les informations à propos de cet utilisateur.",
+ },
+ path: './projects/js-small-apps/users-list/index.html',
+ repo: [
+ {
+ name: 'Github',
+ url: 'https://github.com/ArmandPhilippot/js-small-apps/tree/main/users-list',
+ },
+ {
+ name: 'Gitlab',
+ url: 'https://gitlab.com/ArmandPhilippot/js-small-apps/-/tree/main/users-list',
+ },
+ ],
+ technologies: ['Vanilla Javascript', 'Fetch'],
+ },
+];
+
+export default projects;
diff --git a/src/js/i18n/i18n.js b/src/js/i18n/i18n.js
new file mode 100644
index 0000000..6bdc7cd
--- /dev/null
+++ b/src/js/i18n/i18n.js
@@ -0,0 +1,42 @@
+import I18n from 'i18n-js';
+import en from './locales/en';
+import fr from './locales/fr';
+
+const supportedLanguages = [
+ {
+ code: 'en',
+ label: 'English',
+ translations: en,
+ },
+ {
+ code: 'fr',
+ label: 'Français',
+ translations: fr,
+ },
+];
+
+supportedLanguages.forEach((locale) => {
+ I18n.translations[locale.code] = locale.translations;
+});
+
+function setLocale(locale) {
+ I18n.locale = locale;
+}
+
+function currentLocale() {
+ return I18n.currentLocale();
+}
+
+function translate(name, params = {}) {
+ return I18n.t(name, params);
+}
+
+const { defaultLocale } = I18n;
+
+export {
+ supportedLanguages,
+ setLocale,
+ translate,
+ defaultLocale,
+ currentLocale,
+};
diff --git a/src/js/i18n/locales/en.js b/src/js/i18n/locales/en.js
new file mode 100644
index 0000000..9717528
--- /dev/null
+++ b/src/js/i18n/locales/en.js
@@ -0,0 +1,36 @@
+const en = {
+ branding: {
+ description: 'Front-end developer',
+ },
+ nav: {
+ title: 'Apps list:',
+ },
+ main: {
+ instructions:
+ 'Select an app inside menu to see a live preview and app details (description, technologies, repositories).',
+ project: {
+ details: {
+ about: 'About {{name}}',
+ repo: {
+ one: 'Repository:',
+ other: 'Repositories:',
+ zero: 'Repositories:',
+ },
+ tech: {
+ one: 'Technology:',
+ other: 'Technologies:',
+ zero: 'Technologies:',
+ },
+ },
+ },
+ },
+ footer: {
+ legalNotice: {
+ txt: 'Legal notice',
+ link: 'legal-notice.html',
+ },
+ license: 'License MIT',
+ },
+};
+
+export default en;
diff --git a/src/js/i18n/locales/fr.js b/src/js/i18n/locales/fr.js
new file mode 100644
index 0000000..9c93012
--- /dev/null
+++ b/src/js/i18n/locales/fr.js
@@ -0,0 +1,36 @@
+const fr = {
+ branding: {
+ description: 'Intégrateur web',
+ },
+ nav: {
+ title: 'Liste des applications :',
+ },
+ main: {
+ instructions:
+ "Sélectionnez une application dans le menu pour afficher un aperçu en direct et les informations sur l'application (description, technologies, dépôts).",
+ project: {
+ details: {
+ about: 'À propos de {{name}}',
+ repo: {
+ one: 'Dépôt :',
+ other: 'Dépôts :',
+ zero: 'Dépôt :',
+ },
+ tech: {
+ one: 'Technologie :',
+ other: 'Technologies :',
+ zero: 'Technologie :',
+ },
+ },
+ },
+ },
+ footer: {
+ legalNotice: {
+ txt: 'Mentions légales',
+ link: 'mentions-legales.html',
+ },
+ license: 'Licence MIT',
+ },
+};
+
+export default fr;
diff --git a/src/js/utilities/animations.js b/src/js/utilities/animations.js
new file mode 100644
index 0000000..9a30685
--- /dev/null
+++ b/src/js/utilities/animations.js
@@ -0,0 +1,45 @@
+/**
+ * Change the element classes to hide it with a slide out left animation.
+ * @param {HTMLElement} el - The HTMLElement to hide.
+ */
+function hideToLeft(el) {
+ el?.classList.remove('slide-in--left');
+ el?.classList.add('slide-out--left');
+ setTimeout(() => {
+ el?.classList.add('hide');
+ }, 800);
+}
+
+/**
+ * Change the element classes to show it with a slide in left animation.
+ * @param {HTMLElement} el - The HTMLElement to show.
+ */
+function showFromLeft(el) {
+ el?.classList.remove('slide-out--left');
+ el?.classList.remove('hide');
+ el?.classList.add('slide-in--left');
+}
+
+/**
+ * Change the element classes to hide it with a slide out bottom animation.
+ * @param {HTMLElement} el - The HTMLElement to hide.
+ */
+function hideToBottom(el) {
+ el?.classList.remove('slide-in--up');
+ el?.classList.add('slide-out--bottom');
+ setTimeout(() => {
+ el?.classList.add('hide');
+ }, 800);
+}
+
+/**
+ * Change the element classes to show it with a slide in up animation.
+ * @param {HTMLElement} el - The HTMLElement to show.
+ */
+function showFromBottom(el) {
+ el?.classList.remove('slide-out--bottom');
+ el?.classList.remove('hide');
+ el?.classList.add('slide-in--up');
+}
+
+export { hideToLeft, showFromLeft, hideToBottom, showFromBottom };
diff --git a/src/js/utilities/helpers.js b/src/js/utilities/helpers.js
new file mode 100644
index 0000000..470c49c
--- /dev/null
+++ b/src/js/utilities/helpers.js
@@ -0,0 +1,19 @@
+/**
+ * Check the size of the current viewport.
+ * @returns {Boolean} True if viewport lower than 1200px; false otherwise.
+ */
+function isSmallVw() {
+ return window.innerWidth < 1200;
+}
+
+/**
+ * Check if /assets/styles.js exists (Webpack dev mode).
+ * @returns {Boolean} True if style.js exists ; false otherwise.
+ */
+async function isStyleJsExists() {
+ const filePath = 'assets/js/style.js';
+ const response = await fetch(filePath);
+ return response.status === 200;
+}
+
+export { isSmallVw, isStyleJsExists };