diff options
Diffstat (limited to 'src/js')
| -rw-r--r-- | src/js/app.js | 382 | ||||
| -rw-r--r-- | src/js/config/projects.js | 224 | ||||
| -rw-r--r-- | src/js/i18n/i18n.js | 42 | ||||
| -rw-r--r-- | src/js/i18n/locales/en.js | 36 | ||||
| -rw-r--r-- | src/js/i18n/locales/fr.js | 36 | ||||
| -rw-r--r-- | src/js/utilities/animations.js | 45 | ||||
| -rw-r--r-- | src/js/utilities/helpers.js | 19 |
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 }; |
