From cea30418fa8d7e7749aa2e755e18f99ec6950be8 Mon Sep 17 00:00:00 2001 From: Fernando Tona Date: Sun, 5 Oct 2025 02:58:00 +0100 Subject: [PATCH 01/11] configure eslint and prettier --- .github/workflows/ci.yml | 3 + .prettierignore | 7 + .prettierrc.json | 8 + docs/project-instructions.md | 56 +- eslint.config.mjs | 83 +++ jest.config.js | 13 +- package-lock.json | 1064 +++++++++++++++++++++++++++++++++- package.json | 72 ++- src/css/style.css | 39 +- src/index.html | 173 +++--- src/js/custom-select.js | 62 +- src/js/custom-select.test.js | 64 +- src/js/translations.js | 60 +- src/js/translations.test.js | 89 ++- src/locales/en.json | 2 +- src/locales/es.json | 2 +- src/locales/fr.json | 2 +- src/locales/pt.json | 2 +- 18 files changed, 1487 insertions(+), 314 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 eslint.config.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b9eacd..780d3ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Run ESLint + run: npm run lint + - name: Run Jest Tests run: npm test -- --coverage diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..57a00be --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +node_modules/ +coverage/ +dist/ +build/ +*.min.js +*.min.css +package-lock.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..74686be --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 4, + "trailingComma": "none", + "printWidth": 100, + "arrowParens": "always" +} diff --git a/docs/project-instructions.md b/docs/project-instructions.md index 8797510..d5ee54d 100644 --- a/docs/project-instructions.md +++ b/docs/project-instructions.md @@ -12,33 +12,33 @@ This is a **static portfolio website** built with vanilla HTML, CSS, and JavaScr This module manages all text updates, language detection, and metadata application without external libraries. -* **Logic:** Language priority is checked in this order: **Local Storage (key: 'language') → Browser Language → Fallback to English**. -* **Data Structure:** Translations are stored in flat JSON files within the `/locales/` directory (e.g., `en.json`, `role`). -* **HTML Attributes:** Elements are updated using custom `data-translate` attributes: - * `data-translate="key"`: Updates element's **textContent**. - * `data-translate-alt="key"`: Updates the **alt** attribute (essential for images and accessibility). - * `data-translate-html="key"`: Updates **innerHTML** (supported, currently unused). -* **SEO/Metadata:** The system dynamically updates the page ``, `<meta name="description">`, and Open Graph (`og:title`, `og:description`) tags on language change. -* **Performance:** Translations are loaded **asynchronously** (`async/await`) using the native `fetch` API. +- **Logic:** Language priority is checked in this order: **Local Storage (key: 'language') → Browser Language → Fallback to English**. +- **Data Structure:** Translations are stored in flat JSON files within the `/locales/` directory (e.g., `en.json`, `role`). +- **HTML Attributes:** Elements are updated using custom `data-translate` attributes: + - `data-translate="key"`: Updates element's **textContent**. + - `data-translate-alt="key"`: Updates the **alt** attribute (essential for images and accessibility). + - `data-translate-html="key"`: Updates **innerHTML** (supported, currently unused). +- **SEO/Metadata:** The system dynamically updates the page `<title>`, `<meta name="description">`, and Open Graph (`og:title`, `og:description`) tags on language change. +- **Performance:** Translations are loaded **asynchronously** (`async/await`) using the native `fetch` API. ### Custom Dropdown (`js/custom-select.js`) This file implements the styled dropdown menu for language selection. -* **UI Implementation:** Uses vanilla JavaScript to handle dropdown toggle and visual state management (`.open`, `.selected` classes). -* **Integration:** It integrates with the translation system by calling **`setLanguage(lang)`** from `translations.js` upon selection. -* **Language Names:** Display names (e.g., 'English', 'Español') are hardcoded in the `languageNames` object within this file. +- **UI Implementation:** Uses vanilla JavaScript to handle dropdown toggle and visual state management (`.open`, `.selected` classes). +- **Integration:** It integrates with the translation system by calling **`setLanguage(lang)`** from `translations.js` upon selection. +- **Language Names:** Display names (e.g., 'English', 'Español') are hardcoded in the `languageNames` object within this file. ### CSS Architecture (`css/style.css`) The CSS follows a well-organized structure with clear separation of concerns. -* **Color Scheme:** Uses **CSS Variables** defined in `:root` (e.g., `--clr-navy`, `--clr-linkedin`) for easy theme consistency. -* **Icon Styling Pattern:** Icons (SVGs) use CSS filters for color manipulation across different states: - * White icon on dark background: `filter: brightness(0) invert(1)` - * Dark icon on light background: `filter: brightness(0) invert(0)` -* **Visual Pattern:** Elements like the language selector implement a "Glassmorphism" effect using `rgba()` combined with `backdrop-filter: blur(10px)`. -* **Responsiveness:** Mobile-first approach using Bootstrap utility classes and media queries for specific adjustments below 992px and 576px. +- **Color Scheme:** Uses **CSS Variables** defined in `:root` (e.g., `--clr-navy`, `--clr-linkedin`) for easy theme consistency. +- **Icon Styling Pattern:** Icons (SVGs) use CSS filters for color manipulation across different states: + - White icon on dark background: `filter: brightness(0) invert(1)` + - Dark icon on light background: `filter: brightness(0) invert(0)` +- **Visual Pattern:** Elements like the language selector implement a "Glassmorphism" effect using `rgba()` combined with `backdrop-filter: blur(10px)`. +- **Responsiveness:** Mobile-first approach using Bootstrap utility classes and media queries for specific adjustments below 992px and 576px. --- @@ -46,26 +46,26 @@ The CSS follows a well-organized structure with clear separation of concerns. ### Development Setup -| Task | Detail | -| :--- | :--- | -| **Cross-Platform** | Developed and maintained across **Linux, Windows, and macOS**. | -| **Dependencies** | Requires **Node.js/NPM** to run development tooling (Jest). The core site is dependency-free. | -| **Local Testing** | The project is static: open `index.html` in the browser or use a simple HTTP server. | -| **Deployment** | Git push to the main branch auto-deploys via GitHub Pages. | +| Task | Detail | +| :------------------ | :------------------------------------------------------------------------------------------------------- | +| **Cross-Platform** | Developed and maintained across **Linux, Windows, and macOS**. | +| **Dependencies** | Requires **Node.js/NPM** to run development tooling (Jest). The core site is dependency-free. | +| **Local Testing** | The project is static: open `index.html` in the browser or use a simple HTTP server. | +| **Deployment** | Git push to the main branch auto-deploys via GitHub Pages. | | **Version Control** | **`package-lock.json`** must be committed to Git to ensure dependency stability across all environments. | ### Testing -* **Unit Tests:** JavaScript logic (`translations.js`, `custom-select.js`) is validated by `*.test.js` files. -* **Execution:** Tests must be run using **Jest** via the NPM script: `npm test`. +- **Unit Tests:** JavaScript logic (`translations.js`, `custom-select.js`) is validated by `*.test.js` files. +- **Execution:** Tests must be run using **Jest** via the NPM script: `npm test`. ### Adding New Content -* **New Translations:** +- **New Translations:** 1. Add the new key and value to **ALL** locale files (`en.json`, `es.json`, `fr.json`, `pt.json`). 2. Apply the corresponding `data-translate="keyName"` attribute to the target HTML element. -* **New Languages:** +- **New Languages:** 1. Create a new locale file in `/locales/{lang-code}.json` with **all** existing translation keys. 2. Add the language code to the `supportedLangs` array in `js/translations.js`. 3. Add the language name to the `languageNames` object in `js/custom-select.js`. - 4. Add the option element to the HTML selector in `index.html`. \ No newline at end of file + 4. Add the option element to the HTML selector in `index.html`. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..cb32be2 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,83 @@ +import js from "@eslint/js"; +import globals from "globals"; +import json from "@eslint/json"; +import prettier from "eslint-config-prettier"; +import prettierPlugin from "eslint-plugin-prettier"; + +export default [ + { + ignores: ["coverage/**", "node_modules/**"] + }, + { + files: ["**/*.{js,mjs,cjs}"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: globals.browser + }, + rules: { + ...js.configs.recommended.rules, + "no-unused-vars": "warn", + "no-console": "off" + } + }, + { + files: ["jest.config.js"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "commonjs", + globals: globals.node + }, + rules: { + ...js.configs.recommended.rules + } + }, + { + files: ["**/*.test.js"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { + ...globals.browser, + ...globals.jest, + ...globals.node, + setLanguage: "readonly", + getCurrentLanguage: "readonly" + } + }, + rules: { + ...js.configs.recommended.rules + } + }, + { + files: ["src/js/*.js"], + ignores: ["src/js/*.test.js"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { + ...globals.browser, + setLanguage: "readonly", + getCurrentLanguage: "readonly" + } + }, + rules: { + ...js.configs.recommended.rules + } + }, + { + files: ["**/*.json"], + language: "json/json", + ...json.configs.recommended + }, + prettier, + { + files: ["**/*.{js,mjs,cjs}"], + plugins: { + prettier: prettierPlugin + }, + rules: { + "prettier/prettier": "warn" + } + } +]; \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index a0fb774..c467e6e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,10 +1,7 @@ module.exports = { - testEnvironment: 'jsdom', - testMatch: ['**/*.test.js'], - collectCoverageFrom: [ - 'src/js/**/*.js', - '!src/js/**/*.test.js' - ], - coverageDirectory: 'coverage', - verbose: true + testEnvironment: "jsdom", + testMatch: ["**/*.test.js"], + collectCoverageFrom: ["src/js/**/*.js", "!src/js/**/*.test.js"], + coverageDirectory: "coverage", + verbose: true }; diff --git a/package-lock.json b/package-lock.json index 0230dbc..b4af9c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,16 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { + "@eslint/js": "^9.37.0", + "@eslint/json": "^0.13.2", + "eslint": "^9.37.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-html": "^8.1.3", + "eslint-plugin-prettier": "^5.5.4", + "globals": "^16.4.0", "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0" + "jest-environment-jsdom": "^29.7.0", + "prettier": "^3.6.2" } }, "node_modules/@babel/code-frame": { @@ -509,6 +517,288 @@ "dev": true, "license": "MIT" }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/js": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/json": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@eslint/json/-/json-0.13.2.tgz", + "integrity": "sha512-yWLyRE18rHgHXhWigRpiyv1LDPkvWtC6oa7QHXW7YdP6gosJoq7BiLZW2yCs9U7zN7X4U3ZeOJjepA10XAOIMw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "@eslint/plugin-kit": "^0.3.5", + "@humanwhocodes/momoa": "^3.3.9", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/json/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/json/node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/momoa": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.9.tgz", + "integrity": "sha512-LHw6Op4bJb3/3KZgOgwflJx5zY9XOy0NU1NuyUFKGdTwHYmP+PbnQGCYQJ8NVNlulLfQish34b0VuUlLYP3AXA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -878,6 +1168,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -960,6 +1263,13 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1009,6 +1319,13 @@ "parse5": "^7.0.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.6.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz", @@ -1082,6 +1399,16 @@ "acorn-walk": "^8.0.2" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", @@ -1108,6 +1435,23 @@ "node": ">= 6.0.0" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1693,6 +2037,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -1733,6 +2084,47 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -1747,6 +2139,37 @@ "node": ">=12" } }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1858,49 +2281,280 @@ "hasown": "^2.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-html": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-html/-/eslint-plugin-html-8.1.3.tgz", + "integrity": "sha512-cnCdO7yb/jrvgSJJAfRkGDOwLu1AOvNdw8WCD6nh/2C4RnxuI4tz6QjMEAmmSiHSeugq/fXcIO8yBpIBQrMZCg==", + "dev": true, + "license": "ISC", + "dependencies": { + "htmlparser2": "^10.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": ">=6.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "optionalDependencies": { - "source-map": "~0.6.1" + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esprima": { @@ -1917,6 +2571,32 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -1987,6 +2667,20 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1994,6 +2688,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2004,6 +2705,19 @@ "bser": "2.1.1" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2031,6 +2745,27 @@ "node": ">=8" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -2184,6 +2919,32 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2276,6 +3037,26 @@ "dev": true, "license": "MIT" }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -2328,6 +3109,43 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -2400,6 +3218,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2420,6 +3248,19 @@ "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3245,6 +4086,13 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -3252,6 +4100,20 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3265,6 +4127,16 @@ "node": ">=6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -3285,6 +4157,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -3305,6 +4191,13 @@ "node": ">=8" } }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3515,6 +4408,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3570,6 +4481,19 @@ "node": ">=6" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -3682,6 +4606,45 @@ "node": ">=8" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -4075,6 +5038,22 @@ "dev": true, "license": "MIT" }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -4139,6 +5118,19 @@ "node": ">=12" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -4210,6 +5202,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -4322,6 +5324,16 @@ "node": ">= 8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index a9dafbf..3f7abfb 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,44 @@ { - "name": "fernandotonacoder.github.io", - "version": "1.0.0", - "description": "Fernando Tona's Personal Portfolio Website.", - "directories": { - "doc": "docs" - }, - "scripts": { - "test": "jest" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/fernandotonacoder/fernandotonacoder.github.io.git" - }, - "keywords": [ - "portfolio", - "frontend", - "html", - "css", - "javascript" - ], - "author": "Fernando Tona", - "license": "MIT", - "bugs": { - "url": "https://github.com/fernandotonacoder/fernandotonacoder.github.io/issues" - }, - "homepage": "https://github.com/fernandotonacoder/fernandotonacoder.github.io#readme", - "devDependencies": { - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0" - } + "name": "fernandotonacoder.github.io", + "version": "1.0.0", + "description": "Fernando Tona's Personal Portfolio Website.", + "directories": { + "doc": "docs" + }, + "scripts": { + "test": "jest", + "lint": "eslint \"**/*.js\"", + "lint:fix": "eslint \"**/*.js\" --fix", + "format": "prettier --write \"**/*.{js,json,css,html,md}\"", + "format:check": "prettier --check \"**/*.{js,json,css,html,md}\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/fernandotonacoder/fernandotonacoder.github.io.git" + }, + "keywords": [ + "portfolio", + "frontend", + "html", + "css", + "javascript" + ], + "author": "Fernando Tona", + "license": "MIT", + "bugs": { + "url": "https://github.com/fernandotonacoder/fernandotonacoder.github.io/issues" + }, + "homepage": "https://github.com/fernandotonacoder/fernandotonacoder.github.io#readme", + "devDependencies": { + "@eslint/js": "^9.37.0", + "@eslint/json": "^0.13.2", + "eslint": "^9.37.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-html": "^8.1.3", + "eslint-plugin-prettier": "^5.5.4", + "globals": "^16.4.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "prettier": "^3.6.2" + } } diff --git a/src/css/style.css b/src/css/style.css index bcd1e8f..56f93d5 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -17,11 +17,11 @@ a { /* Color Variables */ :root { - --clr-navy: #070F2B; - --clr-gradient-mid: #3572EF; - --clr-gradient-end: #3ABEF9; - --clr-linkedin: #2856C7; - --clr-white: #FFFFFF; + --clr-navy: #070f2b; + --clr-gradient-mid: #3572ef; + --clr-gradient-end: #3abef9; + --clr-linkedin: #2856c7; + --clr-white: #ffffff; } /* =================================== @@ -30,7 +30,12 @@ a { /* Background and animations */ .gradient-bg { - background: linear-gradient(300deg, var(--clr-navy), var(--clr-gradient-mid), var(--clr-gradient-end)); + background: linear-gradient( + 300deg, + var(--clr-navy), + var(--clr-gradient-mid), + var(--clr-gradient-end) + ); background-size: 180% 180%; animation: gradient-animation 10s ease infinite; } @@ -81,7 +86,7 @@ a { border-radius: 12px; cursor: pointer; transition: all 0.3s ease; - background-image: url('../assets/images/arrow-down.svg'); + background-image: url("../assets/images/arrow-down.svg"); background-repeat: no-repeat; background-position: right 12px center; background-size: 12px; @@ -226,16 +231,16 @@ a { justify-content: center; margin: 0 auto; } - + .col-10.col-sm-8.col-lg-6 img { margin: 0 auto; display: block; } - + .col-lg-6 { text-align: center; } - + .d-grid.gap-2.d-md-flex { justify-content: center !important; } @@ -246,32 +251,32 @@ a { .container { text-align: center; } - + .btn { margin: 0.25rem !important; } - + .language-selector-wrapper { top: 10px; right: 10px; } - + .language-selector { padding: 8px 35px 8px 12px; font-size: 13px; } - + .custom-select { width: 120px; } - + .custom-select-trigger { padding: 8px 35px 8px 12px; font-size: 13px; } - + .custom-option { padding: 8px 12px; font-size: 13px; } -} \ No newline at end of file +} diff --git a/src/index.html b/src/index.html index 866e62f..1faaa7a 100644 --- a/src/index.html +++ b/src/index.html @@ -1,78 +1,119 @@ -<!DOCTYPE html> +<!doctype html> <html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta + name="description" + content="Fernando Tona - Software Developer specializing in .NET/C# and Azure. Problem-Solver with a Musician's Discipline." + /> + <meta name="author" content="Fernando Tona" /> + <meta + name="keywords" + content="Fernando Tona, Software Developer, .NET, C#, Azure, Developer Portfolio" + /> + <title>Fernando Tona - Developer - - - - - - - Fernando Tona - Developer + + + + + - - - - - + - + + + - - - - - - -
-
-
-
-
- English -
-
-
English
-
Español
-
Français
-
Português
+ +
+
+
+
+
+ English +
+
+
English
+
Español
+
Français
+
Português
+
-
-
-
- Fernando Tona picture -
-
-

Fernando Tona

-

Software Developer | .NET/C# | Azure

-

Problem-Solver with a Musician's - Discipline

-
- - - - - - +
+
+ Fernando Tona picture +
+
+

+ Fernando Tona +

+

+ Software Developer | .NET/C# | Azure +

+

+ Problem-Solver with a Musician's Discipline +

+
-
- -
- - - - +
- \ No newline at end of file + + + + diff --git a/src/js/custom-select.js b/src/js/custom-select.js index c1a5ce6..4efab1f 100644 --- a/src/js/custom-select.js +++ b/src/js/custom-select.js @@ -1,43 +1,45 @@ -document.addEventListener('DOMContentLoaded', () => { - const customSelect = document.querySelector('.custom-select'); - const trigger = document.getElementById('languageTrigger'); - const options = document.getElementById('languageOptions'); - const selectedLanguageSpan = document.getElementById('selectedLanguage'); - +document.addEventListener("DOMContentLoaded", () => { + const customSelect = document.querySelector(".custom-select"); + const trigger = document.getElementById("languageTrigger"); + const options = document.getElementById("languageOptions"); + const selectedLanguageSpan = document.getElementById("selectedLanguage"); + const languageNames = { - en: 'English', - es: 'Español', - fr: 'Français', - pt: 'Português' + en: "English", + es: "Español", + fr: "Français", + pt: "Português" }; - - trigger.addEventListener('click', (e) => { + + trigger.addEventListener("click", (e) => { e.stopPropagation(); - customSelect.classList.toggle('open'); + customSelect.classList.toggle("open"); }); - - document.addEventListener('click', () => { - customSelect.classList.remove('open'); + + document.addEventListener("click", () => { + customSelect.classList.remove("open"); }); - - options.addEventListener('click', async (e) => { - if (e.target.classList.contains('custom-option')) { - const selectedValue = e.target.getAttribute('data-value'); - - document.querySelectorAll('.custom-option').forEach(opt => { - opt.classList.remove('selected'); + + options.addEventListener("click", async (e) => { + if (e.target.classList.contains("custom-option")) { + const selectedValue = e.target.getAttribute("data-value"); + + document.querySelectorAll(".custom-option").forEach((opt) => { + opt.classList.remove("selected"); }); - e.target.classList.add('selected'); - + e.target.classList.add("selected"); + selectedLanguageSpan.textContent = languageNames[selectedValue]; - - customSelect.classList.remove('open'); - + + customSelect.classList.remove("open"); + await setLanguage(selectedValue); } }); - + const currentLang = getCurrentLanguage(); selectedLanguageSpan.textContent = languageNames[currentLang]; - document.querySelector(`.custom-option[data-value="${currentLang}"]`)?.classList.add('selected'); + document + .querySelector(`.custom-option[data-value="${currentLang}"]`) + ?.classList.add("selected"); }); diff --git a/src/js/custom-select.test.js b/src/js/custom-select.test.js index 629d3c0..6e63be3 100644 --- a/src/js/custom-select.test.js +++ b/src/js/custom-select.test.js @@ -3,9 +3,9 @@ */ global.setLanguage = jest.fn().mockResolvedValue(undefined); -global.getCurrentLanguage = jest.fn().mockReturnValue('en'); +global.getCurrentLanguage = jest.fn().mockReturnValue("en"); -describe('Custom Select Component', () => { +describe("Custom Select Component", () => { let customSelect, trigger, selectedLanguageSpan; beforeEach(() => { @@ -23,58 +23,58 @@ describe('Custom Select Component', () => { `; - customSelect = document.querySelector('.custom-select'); - trigger = document.getElementById('languageTrigger'); - selectedLanguageSpan = document.getElementById('selectedLanguage'); + customSelect = document.querySelector(".custom-select"); + trigger = document.getElementById("languageTrigger"); + selectedLanguageSpan = document.getElementById("selectedLanguage"); jest.clearAllMocks(); - require('./custom-select.js'); - document.dispatchEvent(new Event('DOMContentLoaded')); + require("./custom-select.js"); + document.dispatchEvent(new Event("DOMContentLoaded")); }); afterEach(() => { - document.body.innerHTML = ''; + document.body.innerHTML = ""; }); - test('should initialize with current language and mark it as selected', () => { + test("should initialize with current language and mark it as selected", () => { expect(getCurrentLanguage).toHaveBeenCalled(); - expect(selectedLanguageSpan.textContent).toBe('English'); - - const selectedOption = document.querySelector('.custom-option.selected'); - expect(selectedOption.getAttribute('data-value')).toBe('en'); + expect(selectedLanguageSpan.textContent).toBe("English"); + + const selectedOption = document.querySelector(".custom-option.selected"); + expect(selectedOption.getAttribute("data-value")).toBe("en"); }); - test('should toggle dropdown when trigger is clicked', () => { - expect(customSelect.classList.contains('open')).toBe(false); - + test("should toggle dropdown when trigger is clicked", () => { + expect(customSelect.classList.contains("open")).toBe(false); + trigger.click(); - expect(customSelect.classList.contains('open')).toBe(true); - + expect(customSelect.classList.contains("open")).toBe(true); + trigger.click(); - expect(customSelect.classList.contains('open')).toBe(false); + expect(customSelect.classList.contains("open")).toBe(false); }); - test('should close dropdown when clicking outside', () => { + test("should close dropdown when clicking outside", () => { trigger.click(); - expect(customSelect.classList.contains('open')).toBe(true); - + expect(customSelect.classList.contains("open")).toBe(true); + document.body.click(); - expect(customSelect.classList.contains('open')).toBe(false); + expect(customSelect.classList.contains("open")).toBe(false); }); - test('should change language and update UI when option is selected', async () => { + test("should change language and update UI when option is selected", async () => { const spanishOption = document.querySelector('.custom-option[data-value="es"]'); const englishOption = document.querySelector('.custom-option[data-value="en"]'); - + trigger.click(); await spanishOption.click(); - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(setLanguage).toHaveBeenCalledWith('es'); - expect(selectedLanguageSpan.textContent).toBe('Español'); - expect(spanishOption.classList.contains('selected')).toBe(true); - expect(englishOption.classList.contains('selected')).toBe(false); - expect(customSelect.classList.contains('open')).toBe(false); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(setLanguage).toHaveBeenCalledWith("es"); + expect(selectedLanguageSpan.textContent).toBe("Español"); + expect(spanishOption.classList.contains("selected")).toBe(true); + expect(englishOption.classList.contains("selected")).toBe(false); + expect(customSelect.classList.contains("open")).toBe(false); }); }); diff --git a/src/js/translations.js b/src/js/translations.js index 9dc7e1a..d7055c0 100644 --- a/src/js/translations.js +++ b/src/js/translations.js @@ -3,71 +3,75 @@ let translations = {}; async function loadTranslations(lang) { try { const response = await fetch(`./locales/${lang}.json`); - if (!response.ok) throw new Error('Translation file not found'); + if (!response.ok) throw new Error("Translation file not found"); return await response.json(); } catch (error) { console.error(`Failed to load translations for ${lang}:`, error); // Fallback to English - if (lang !== 'en') { - return await loadTranslations('en'); + if (lang !== "en") { + return await loadTranslations("en"); } return {}; } } function getNestedProperty(obj, path) { - return path.split('.').reduce((current, key) => current?.[key], obj); + return path.split(".").reduce((current, key) => current?.[key], obj); } function getBrowserLanguage() { - const browserLang = navigator.language.split('-')[0]; - const supportedLangs = ['en', 'es', 'fr', 'pt']; - return supportedLangs.includes(browserLang) ? browserLang : 'en'; + const browserLang = navigator.language.split("-")[0]; + const supportedLangs = ["en", "es", "fr", "pt"]; + return supportedLangs.includes(browserLang) ? browserLang : "en"; } function getCurrentLanguage() { - return localStorage.getItem('language') || getBrowserLanguage(); + return localStorage.getItem("language") || getBrowserLanguage(); } async function setLanguage(lang) { translations = await loadTranslations(lang); - - localStorage.setItem('language', lang); - + + localStorage.setItem("language", lang); + document.title = translations.title; - - document.querySelector('meta[name="description"]').setAttribute('content', translations.metaDescription); - document.querySelector('meta[property="og:description"]').setAttribute('content', translations.metaDescription); - document.querySelector('meta[property="og:title"]').setAttribute('content', translations.title); - - document.querySelectorAll('[data-translate]').forEach(element => { - const key = element.getAttribute('data-translate'); + + document + .querySelector('meta[name="description"]') + .setAttribute("content", translations.metaDescription); + document + .querySelector('meta[property="og:description"]') + .setAttribute("content", translations.metaDescription); + document.querySelector('meta[property="og:title"]').setAttribute("content", translations.title); + + document.querySelectorAll("[data-translate]").forEach((element) => { + const key = element.getAttribute("data-translate"); const value = getNestedProperty(translations, key); if (value) { element.textContent = value; } }); - - document.querySelectorAll('[data-translate-alt]').forEach(element => { - const key = element.getAttribute('data-translate-alt'); + + document.querySelectorAll("[data-translate-alt]").forEach((element) => { + const key = element.getAttribute("data-translate-alt"); const value = getNestedProperty(translations, key); if (value) { - element.setAttribute('alt', value); + element.setAttribute("alt", value); } }); - - document.querySelectorAll('[data-translate-html]').forEach(element => { - const key = element.getAttribute('data-translate-html'); + + document.querySelectorAll("[data-translate-html]").forEach((element) => { + const key = element.getAttribute("data-translate-html"); const value = getNestedProperty(translations, key); if (value) { element.innerHTML = value; } }); - - document.documentElement.setAttribute('lang', lang); + + document.documentElement.setAttribute("lang", lang); } -document.addEventListener('DOMContentLoaded', async () => { +document.addEventListener("DOMContentLoaded", async () => { const currentLang = getCurrentLanguage(); await setLanguage(currentLang); }); diff --git a/src/js/translations.test.js b/src/js/translations.test.js index c5a6441..9cd453a 100644 --- a/src/js/translations.test.js +++ b/src/js/translations.test.js @@ -5,75 +5,74 @@ global.fetch = jest.fn(); console.error = jest.fn(); -describe('Translations Module', () => { - +describe("Translations Module", () => { beforeEach(() => { jest.clearAllMocks(); localStorage.clear(); }); - describe('Core Helper Functions', () => { - test('getNestedProperty - should handle nested translation keys', () => { - const getNestedProperty = (obj, path) => - path.split('.').reduce((current, key) => current?.[key], obj); + describe("Core Helper Functions", () => { + test("getNestedProperty - should handle nested translation keys", () => { + const getNestedProperty = (obj, path) => + path.split(".").reduce((current, key) => current?.[key], obj); - const translations = { user: { profile: { name: 'Jane' } } }; - - expect(getNestedProperty(translations, 'user.profile.name')).toBe('Jane'); - expect(getNestedProperty(translations, 'missing.key')).toBeUndefined(); + const translations = { user: { profile: { name: "Jane" } } }; + + expect(getNestedProperty(translations, "user.profile.name")).toBe("Jane"); + expect(getNestedProperty(translations, "missing.key")).toBeUndefined(); }); - test('getBrowserLanguage - should return supported language or default to English', () => { + test("getBrowserLanguage - should return supported language or default to English", () => { const getBrowserLanguage = () => { - const browserLang = navigator.language.split('-')[0]; - const supportedLangs = ['en', 'es', 'fr', 'pt']; - return supportedLangs.includes(browserLang) ? browserLang : 'en'; + const browserLang = navigator.language.split("-")[0]; + const supportedLangs = ["en", "es", "fr", "pt"]; + return supportedLangs.includes(browserLang) ? browserLang : "en"; }; - Object.defineProperty(navigator, 'language', { value: 'es-ES', configurable: true }); - expect(getBrowserLanguage()).toBe('es'); + Object.defineProperty(navigator, "language", { value: "es-ES", configurable: true }); + expect(getBrowserLanguage()).toBe("es"); - Object.defineProperty(navigator, 'language', { value: 'de-DE', configurable: true }); - expect(getBrowserLanguage()).toBe('en'); + Object.defineProperty(navigator, "language", { value: "de-DE", configurable: true }); + expect(getBrowserLanguage()).toBe("en"); }); }); - describe('Translation Loading', () => { - test('should successfully fetch translations', async () => { - const mockData = { title: 'Test Title', role: 'Developer' }; + describe("Translation Loading", () => { + test("should successfully fetch translations", async () => { + const mockData = { title: "Test Title", role: "Developer" }; global.fetch.mockResolvedValueOnce({ ok: true, json: async () => mockData }); - const response = await fetch('./locales/en.json'); + const response = await fetch("./locales/en.json"); const data = await response.json(); expect(data).toEqual(mockData); }); - test('should handle fetch errors gracefully', async () => { - global.fetch.mockRejectedValueOnce(new Error('Network error')); + test("should handle fetch errors gracefully", async () => { + global.fetch.mockRejectedValueOnce(new Error("Network error")); try { - await fetch('./locales/es.json'); + await fetch("./locales/es.json"); } catch (error) { - expect(error.message).toBe('Network error'); + expect(error.message).toBe("Network error"); } }); }); - describe('localStorage Integration', () => { - test('should store and retrieve selected language', () => { - localStorage.setItem('language', 'es'); - expect(localStorage.getItem('language')).toBe('es'); - - localStorage.setItem('language', 'fr'); - expect(localStorage.getItem('language')).toBe('fr'); + describe("localStorage Integration", () => { + test("should store and retrieve selected language", () => { + localStorage.setItem("language", "es"); + expect(localStorage.getItem("language")).toBe("es"); + + localStorage.setItem("language", "fr"); + expect(localStorage.getItem("language")).toBe("fr"); }); }); - describe('DOM Updates', () => { + describe("DOM Updates", () => { beforeEach(() => { document.documentElement.innerHTML = ` @@ -87,27 +86,27 @@ describe('Translations Module', () => { `; }); - test('should update document title and meta tags', () => { - document.title = 'New Title'; + test("should update document title and meta tags", () => { + document.title = "New Title"; const metaDesc = document.querySelector('meta[name="description"]'); - metaDesc.setAttribute('content', 'New Description'); + metaDesc.setAttribute("content", "New Description"); - expect(document.title).toBe('New Title'); - expect(metaDesc.getAttribute('content')).toBe('New Description'); + expect(document.title).toBe("New Title"); + expect(metaDesc.getAttribute("content")).toBe("New Description"); }); - test('should update elements with data-translate attribute', () => { + test("should update elements with data-translate attribute", () => { const element = document.querySelector('[data-translate="role"]'); - element.textContent = 'Software Developer'; + element.textContent = "Software Developer"; - expect(element.textContent).toBe('Software Developer'); + expect(element.textContent).toBe("Software Developer"); }); - test('should update alt attributes with data-translate-alt', () => { + test("should update alt attributes with data-translate-alt", () => { const img = document.querySelector('[data-translate-alt="imageAlt"]'); - img.setAttribute('alt', 'New Alt Text'); + img.setAttribute("alt", "New Alt Text"); - expect(img.getAttribute('alt')).toBe('New Alt Text'); + expect(img.getAttribute("alt")).toBe("New Alt Text"); }); }); }); diff --git a/src/locales/en.json b/src/locales/en.json index 2016572..fce3641 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -4,4 +4,4 @@ "role": "Software Developer | .NET/C# | Azure", "tagline": "Problem-Solver with a Musician's Discipline", "imageAlt": "Fernando Tona picture" -} \ No newline at end of file +} diff --git a/src/locales/es.json b/src/locales/es.json index dd46719..fff7dfa 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -4,4 +4,4 @@ "role": "Desarrollador de Software | .NET/C# | Azure", "tagline": "Solucionador de Problemas con Disciplina de Músico", "imageAlt": "Foto de Fernando Tona" -} \ No newline at end of file +} diff --git a/src/locales/fr.json b/src/locales/fr.json index 3662896..ccaad25 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -4,4 +4,4 @@ "role": "Développeur Logiciel | .NET/C# | Azure", "tagline": "Résolveur de Problèmes avec une Discipline de Musicien", "imageAlt": "Photo de Fernando Tona" -} \ No newline at end of file +} diff --git a/src/locales/pt.json b/src/locales/pt.json index b5637c7..06e3e87 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -4,4 +4,4 @@ "role": "Desenvolvedor de Software | .NET/C# | Azure", "tagline": "Solucionador de Problemas com Disciplina de Músico", "imageAlt": "Foto de Fernando Tona" -} \ No newline at end of file +} From 601b81b1ea86dccfe81e7fa83c2182ce173ea100 Mon Sep 17 00:00:00 2001 From: Fernando Tona Date: Sun, 5 Oct 2025 03:35:05 +0100 Subject: [PATCH 02/11] improve CI/CD --- .github/workflows/ci-cd.yml | 75 +++++++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 32 ---------------- 2 files changed, 75 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/ci-cd.yml delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..ec222dc --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,75 @@ +name: CI/CD Pipeline + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Install dependencies + run: npm ci + + - name: Check code formatting + run: npm run format:check + + - name: Run ESLint + run: npm run lint + + - name: Run Tests + run: npm test -- --coverage + + - name: Upload Test Coverage Artifact + uses: actions/upload-artifact@v4 + with: + name: jest-coverage-report + path: coverage/ + + build: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: "./src" + + deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 780d3ef..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: CI & Test - -on: - pull_request: - branches: [ main ] - -jobs: - test_and_lint: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '24' - - - name: Install dependencies - run: npm ci - - - name: Run ESLint - run: npm run lint - - - name: Run Jest Tests - run: npm test -- --coverage - - - name: Upload Test Coverage Artifact - uses: actions/upload-artifact@v4 - with: - name: jest-coverage-report - path: coverage/ \ No newline at end of file From 068383bd58678a0eb7df66b8835ba3dd108e23cd Mon Sep 17 00:00:00 2001 From: Fernando Tona Date: Sun, 5 Oct 2025 04:00:06 +0100 Subject: [PATCH 03/11] first readme version --- README.md | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6c1da8 --- /dev/null +++ b/README.md @@ -0,0 +1,253 @@ +# Fernando Tona - Portfolio Website 🚀 + +[![CI/CD Pipeline](https://github.com/fernandotonacoder/fernandotonacoder.github.io/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/fernandotonacoder/fernandotonacoder.github.io/actions/workflows/ci-cd.yml) +[![GitHub Pages](https://img.shields.io/badge/GitHub%20Pages-deployed-success?logo=github)](https://fernandotonacoder.github.io) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Code Style: Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) +[![Tested with Jest](https://img.shields.io/badge/tested_with-jest-99424f.svg?logo=jest)](https://github.com/facebook/jest) + +> Personal portfolio website showcasing my work as a Software Developer specializing in .NET/C# and Azure. + +**🌐 Live Site:** [fernandotonacoder.github.io](https://fernandotonacoder.github.io) + +--- + +## ✨ Features + +- 🌍 **Multi-language Support** - English, Spanish, French, and Portuguese +- 🎨 **Modern Design** - Clean, responsive interface with glassmorphism effects +- 📱 **Mobile-First** - Fully responsive across all devices +- ♿ **Accessible** - Semantic HTML and ARIA attributes +- 🚀 **Lightweight** - No heavy frameworks, pure vanilla JavaScript +- ⚡ **Fast** - Static site hosted on GitHub Pages +- 🔧 **Well-Tested** - Unit tests with Jest for core functionality +- 🎯 **SEO Optimized** - Dynamic meta tags and Open Graph support + +--- + +## 🛠️ Tech Stack + +### Frontend +- **HTML5** - Semantic markup +- **CSS3** - Custom styles with CSS Variables +- **JavaScript (ES6+)** - Vanilla JS, no frameworks +- **Bootstrap 5** - Layout and utilities + +### Development & Quality +- **Jest** - Unit testing +- **ESLint** - Code linting +- **Prettier** - Code formatting +- **GitHub Actions** - CI/CD pipeline + +### Deployment +- **GitHub Pages** - Static site hosting + +--- + +## 🚀 Quick Start + +### For Non-Technical Users + +Simply visit the live site: **[fernandotonacoder.github.io](https://fernandotonacoder.github.io)** + +### For Developers + +```bash +# Clone the repository +git clone https://github.com/fernandotonacoder/fernandotonacoder.github.io.git +cd fernandotonacoder.github.io + +# Install dependencies (for testing/linting only) +npm install + +# Run tests +npm test + +# Check code quality +npm run lint +npm run format:check + +# Open the site locally +# Option 1: Simply open src/index.html in your browser +# Option 2: Use a simple HTTP server +python -m http.server 8000 -d src +# Then visit http://localhost:8000 +``` + +--- + +## 📂 Project Structure + +``` +. +├── src/ +│ ├── index.html # Main HTML file +│ ├── css/ +│ │ └── style.css # Custom styles +│ ├── js/ +│ │ ├── translations.js # Multi-language system +│ │ ├── custom-select.js # Language selector dropdown +│ │ └── *.test.js # Unit tests +│ ├── locales/ +│ │ ├── en.json # English translations +│ │ ├── es.json # Spanish translations +│ │ ├── fr.json # French translations +│ │ └── pt.json # Portuguese translations +│ └── assets/ +│ └── images/ # Images and icons +├── .github/ +│ └── workflows/ +│ └── ci-cd.yml # CI/CD pipeline +├── docs/ +│ └── project-instructions.md # Detailed technical documentation +└── package.json # Dependencies and scripts +``` + +--- + +## 🌍 Multi-Language System + +The site automatically detects your browser language and displays content accordingly. You can manually switch languages using the dropdown selector. + +**Supported Languages:** +- 🇺🇸 English +- 🇪🇸 Spanish (Español) +- 🇫🇷 French (Français) +- 🇧🇷 Portuguese (Português) + +**How it works:** +1. Checks your saved preference (localStorage) +2. Falls back to browser language +3. Defaults to English if language not supported + +--- + +## 🧪 Testing & Quality Assurance + +This project maintains high code quality standards: + +```bash +# Run all tests with coverage +npm test + +# Lint JavaScript code +npm run lint + +# Auto-fix linting issues +npm run lint:fix + +# Check code formatting +npm run format:check + +# Format all files +npm run format +``` + +**Test Coverage:** +- ✅ Translation system logic +- ✅ Language detection +- ✅ DOM manipulation +- ✅ Custom dropdown functionality + +--- + +## 🔄 CI/CD Pipeline + +Every pull request and merge to `main` triggers automated checks: + +### On Pull Request: +- ✅ Code formatting validation (Prettier) +- ✅ Code linting (ESLint) +- ✅ Unit tests (Jest) +- ✅ Test coverage report + +### On Merge to Main: +- ✅ All PR checks +- ✅ Build for GitHub Pages +- ✅ Automatic deployment 🚀 + +**Branch Protection:** +- Direct pushes to `main` are blocked +- All tests must pass before merging +- Branches must be up to date +- Linear history enforced (squash merges) + +--- + +## 🎨 Design Highlights + +- **Color Scheme:** Navy blue primary with accent colors +- **Typography:** System fonts for optimal performance +- **Effects:** Glassmorphism on language selector +- **Icons:** SVG icons with CSS filters for color manipulation +- **Layout:** Bootstrap grid with custom adjustments +- **Animations:** Smooth transitions and hover effects + +--- + +## 📝 Development Notes + +### Code Style +- **Quotes:** Double quotes for strings +- **Semicolons:** Always required +- **Indentation:** 4 spaces +- **Arrow Functions:** Always use parentheses +- **Trailing Commas:** None + +### Adding New Features + +**New Translation:** +1. Add key-value to all JSON files in `src/locales/` +2. Add `data-translate="key"` attribute to HTML element + +**New Language:** +1. Create `src/locales/{lang-code}.json` +2. Add language code to `supportedLangs` in `translations.js` +3. Add language name to `languageNames` in `custom-select.js` +4. Add option to HTML selector + +--- + +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +--- + +## 👤 Author + +**Fernando Tona** + +- 🌐 Website: [fernandotonacoder.github.io](https://fernandotonacoder.github.io) +- 💼 LinkedIn: [Fernando Tona](https://www.linkedin.com/in/fernandotona/) +- 🐙 GitHub: [@fernandotonacoder](https://github.com/fernandotonacoder) + +--- + +## 🤝 Contributing + +Contributions, issues, and feature requests are welcome! + +1. Fork the project +2. Create your feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +**Note:** All PRs must pass automated tests and code quality checks. + +--- + +## 📚 Additional Documentation + +For detailed technical documentation, see [docs/project-instructions.md](docs/project-instructions.md) + +--- + +
+ +**⭐ Star this repo if you found it useful!** + +Made with ❤️ by Fernando Tona + +
From ba2c2fe9fecb147584db4c39710fbe4fac213675 Mon Sep 17 00:00:00 2001 From: Fernando Tona Date: Sun, 5 Oct 2025 04:15:41 +0100 Subject: [PATCH 04/11] update readme and docs --- README.md | 236 ++++++--------------------- docs/design.md | 376 +++++++++++++++++++++++++++++++++++++++++++ docs/development.md | 377 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 798 insertions(+), 191 deletions(-) create mode 100644 docs/design.md create mode 100644 docs/development.md diff --git a/README.md b/README.md index a6c1da8..035c77b 100644 --- a/README.md +++ b/README.md @@ -14,239 +14,93 @@ ## ✨ Features -- 🌍 **Multi-language Support** - English, Spanish, French, and Portuguese -- 🎨 **Modern Design** - Clean, responsive interface with glassmorphism effects -- 📱 **Mobile-First** - Fully responsive across all devices -- ♿ **Accessible** - Semantic HTML and ARIA attributes -- 🚀 **Lightweight** - No heavy frameworks, pure vanilla JavaScript -- ⚡ **Fast** - Static site hosted on GitHub Pages -- 🔧 **Well-Tested** - Unit tests with Jest for core functionality -- 🎯 **SEO Optimized** - Dynamic meta tags and Open Graph support - ---- +- 🌍 Multi-language support (English, Spanish, French, Portuguese) +- 🎨 Modern, responsive design with glassmorphism effects +- Lightweight vanilla JavaScript - no frameworks +- ⚡ Fast static site hosted on GitHub Pages +- 🔧 Well-tested with Jest +- 🎯 SEO optimized with dynamic meta tags ## 🛠️ Tech Stack -### Frontend -- **HTML5** - Semantic markup -- **CSS3** - Custom styles with CSS Variables -- **JavaScript (ES6+)** - Vanilla JS, no frameworks -- **Bootstrap 5** - Layout and utilities - -### Development & Quality -- **Jest** - Unit testing -- **ESLint** - Code linting -- **Prettier** - Code formatting -- **GitHub Actions** - CI/CD pipeline - -### Deployment -- **GitHub Pages** - Static site hosting - ---- +**Frontend:** HTML5 • CSS3 • Vanilla JavaScript • Bootstrap 5 +**Development:** Jest • ESLint • Prettier +**CI/CD:** GitHub Actions +**Deployment:** GitHub Pages ## 🚀 Quick Start -### For Non-Technical Users - -Simply visit the live site: **[fernandotonacoder.github.io](https://fernandotonacoder.github.io)** +### For Visitors +Simply visit: **[fernandotonacoder.github.io](https://fernandotonacoder.github.io)** ### For Developers - ```bash -# Clone the repository +# Clone and install git clone https://github.com/fernandotonacoder/fernandotonacoder.github.io.git cd fernandotonacoder.github.io - -# Install dependencies (for testing/linting only) npm install -# Run tests +# Run tests and checks npm test - -# Check code quality npm run lint npm run format:check -# Open the site locally -# Option 1: Simply open src/index.html in your browser -# Option 2: Use a simple HTTP server +# Open locally (Option 1: directly in browser) +open src/index.html + +# Or Option 2: with local server python -m http.server 8000 -d src -# Then visit http://localhost:8000 ``` ---- - ## 📂 Project Structure ``` -. -├── src/ -│ ├── index.html # Main HTML file -│ ├── css/ -│ │ └── style.css # Custom styles -│ ├── js/ -│ │ ├── translations.js # Multi-language system -│ │ ├── custom-select.js # Language selector dropdown -│ │ └── *.test.js # Unit tests -│ ├── locales/ -│ │ ├── en.json # English translations -│ │ ├── es.json # Spanish translations -│ │ ├── fr.json # French translations -│ │ └── pt.json # Portuguese translations -│ └── assets/ -│ └── images/ # Images and icons -├── .github/ -│ └── workflows/ -│ └── ci-cd.yml # CI/CD pipeline -├── docs/ -│ └── project-instructions.md # Detailed technical documentation -└── package.json # Dependencies and scripts +src/ +├── index.html # Main page +├── css/style.css # Styles +├── js/ +│ ├── translations.js # Multi-language system +│ ├── custom-select.js # Language selector +│ └── *.test.js # Unit tests +└── locales/ # Translation files (en, es, fr, pt) ``` ---- - -## 🌍 Multi-Language System - -The site automatically detects your browser language and displays content accordingly. You can manually switch languages using the dropdown selector. - -**Supported Languages:** -- 🇺🇸 English -- 🇪🇸 Spanish (Español) -- 🇫🇷 French (Français) -- 🇧🇷 Portuguese (Português) - -**How it works:** -1. Checks your saved preference (localStorage) -2. Falls back to browser language -3. Defaults to English if language not supported - ---- - -## 🧪 Testing & Quality Assurance - -This project maintains high code quality standards: +## 🧪 Development ```bash -# Run all tests with coverage -npm test - -# Lint JavaScript code -npm run lint - -# Auto-fix linting issues -npm run lint:fix - -# Check code formatting -npm run format:check - -# Format all files -npm run format +npm test # Run tests +npm run lint # Check code quality +npm run lint:fix # Auto-fix issues +npm run format # Format code +npm run format:check # Check formatting ``` -**Test Coverage:** -- ✅ Translation system logic -- ✅ Language detection -- ✅ DOM manipulation -- ✅ Custom dropdown functionality +## 🔄 CI/CD ---- - -## 🔄 CI/CD Pipeline - -Every pull request and merge to `main` triggers automated checks: - -### On Pull Request: -- ✅ Code formatting validation (Prettier) -- ✅ Code linting (ESLint) -- ✅ Unit tests (Jest) -- ✅ Test coverage report - -### On Merge to Main: -- ✅ All PR checks -- ✅ Build for GitHub Pages -- ✅ Automatic deployment 🚀 - -**Branch Protection:** -- Direct pushes to `main` are blocked -- All tests must pass before merging -- Branches must be up to date -- Linear history enforced (squash merges) - ---- - -## 🎨 Design Highlights - -- **Color Scheme:** Navy blue primary with accent colors -- **Typography:** System fonts for optimal performance -- **Effects:** Glassmorphism on language selector -- **Icons:** SVG icons with CSS filters for color manipulation -- **Layout:** Bootstrap grid with custom adjustments -- **Animations:** Smooth transitions and hover effects - ---- - -## 📝 Development Notes - -### Code Style -- **Quotes:** Double quotes for strings -- **Semicolons:** Always required -- **Indentation:** 4 spaces -- **Arrow Functions:** Always use parentheses -- **Trailing Commas:** None - -### Adding New Features - -**New Translation:** -1. Add key-value to all JSON files in `src/locales/` -2. Add `data-translate="key"` attribute to HTML element - -**New Language:** -1. Create `src/locales/{lang-code}.json` -2. Add language code to `supportedLangs` in `translations.js` -3. Add language name to `languageNames` in `custom-select.js` -4. Add option to HTML selector - ---- - -## 📄 License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - ---- +- **On PR:** Runs tests, linting, and format checks +- **On Merge:** Deploys to GitHub Pages automatically +- **Branch Protection:** All checks must pass before merging ## 👤 Author -**Fernando Tona** +**Fernando Tona** +🌐 [Website](https://fernandotonacoder.github.io) • 💼 [LinkedIn](https://www.linkedin.com/in/fernandotona/) • 🐙 [GitHub](https://github.com/fernandotonacoder) -- 🌐 Website: [fernandotonacoder.github.io](https://fernandotonacoder.github.io) -- 💼 LinkedIn: [Fernando Tona](https://www.linkedin.com/in/fernandotona/) -- 🐙 GitHub: [@fernandotonacoder](https://github.com/fernandotonacoder) +## 📚 Documentation ---- - -## 🤝 Contributing - -Contributions, issues, and feature requests are welcome! - -1. Fork the project -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request - -**Note:** All PRs must pass automated tests and code quality checks. - ---- +- **[Technical Documentation](docs/project-instructions.md)** - Architecture, conventions, and detailed guides +- **[Development Guide](docs/development.md)** - Setup, testing, and contribution guidelines +- **[Design System](docs/design.md)** - Colors, typography, and styling patterns -## 📚 Additional Documentation +## � License -For detailed technical documentation, see [docs/project-instructions.md](docs/project-instructions.md) +MIT License - see [LICENSE](LICENSE) for details. ---
-**⭐ Star this repo if you found it useful!** +**⭐ Star this repo if you find it useful!** Made with ❤️ by Fernando Tona diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..91fa53e --- /dev/null +++ b/docs/design.md @@ -0,0 +1,376 @@ +# Design System + +## Color Palette + +### Primary Colors + +```css +:root { + --clr-navy: #001f3f; /* Primary background */ + --clr-navy-text: #001f3fef; /* Text on light backgrounds */ + --clr-blue-text: #2563eb; /* Accent text */ + --clr-white: #fff; /* Primary text on dark */ + --clr-white-soft: #ffffffda; /* Secondary text on dark */ +} +``` + +### Social Media Colors + +```css +:root { + --clr-linkedin: #0077b5; /* LinkedIn blue */ + --clr-github: #333; /* GitHub dark */ +} +``` + +### Usage + +**Dark Sections (Navy Background):** +- Background: `var(--clr-navy)` +- Primary text: `var(--clr-white)` +- Secondary text: `var(--clr-white-soft)` + +**Light Sections (White Background):** +- Background: `var(--clr-white)` +- Primary text: `var(--clr-navy-text)` +- Accent text: `var(--clr-blue-text)` + +--- + +## Typography + +### Font Stack + +```css +font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", + "Noto Sans", "Liberation Sans", Arial, sans-serif; +``` + +**Why system fonts?** +- ✅ No external font loading (faster) +- ✅ Native look on each OS +- ✅ Better performance +- ✅ Excellent readability + +### Font Sizes + +- **Headings:** Use Bootstrap classes (`.display-*`, `.h1` - `.h6`) +- **Body:** Default browser size (16px base) +- **Small text:** `.small` or custom sizing + +--- + +## Layout + +### Grid System + +Uses **Bootstrap 5 Grid**: +- 12-column responsive grid +- Breakpoints: xs, sm, md, lg, xl, xxl +- Container classes: `.container`, `.container-fluid` + +### Spacing + +Bootstrap utility classes: +```html + +
+
+
+ + +
+
+
+``` + +**Scale:** 0-5 (0 = 0, 1 = 0.25rem, 2 = 0.5rem, 3 = 1rem, 4 = 1.5rem, 5 = 3rem) + +--- + +## Components + +### Language Selector + +**Glass morphism effect:** +```css +.custom-select { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; +} +``` + +**States:** +- Default: Semi-transparent white +- Hover: Slightly more opaque +- Open: Dropdown visible +- Selected: Highlighted option + +### Buttons + +**Primary Button:** +```css +.btn-primary { + background-color: var(--clr-blue-text); + border: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; + transition: all 0.3s ease; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} +``` + +### Icons + +**SVG Color Manipulation:** +```css +/* White icon on dark background */ +.icon-white { + filter: brightness(0) invert(1); +} + +/* Dark icon on light background */ +.icon-dark { + filter: brightness(0) invert(0); +} + +/* Specific color (LinkedIn blue) */ +.icon-linkedin { + filter: invert(39%) sepia(57%) saturate(2878%) + hue-rotate(178deg) brightness(93%) contrast(101%); +} +``` + +**How to generate filter values:** +Use [this tool](https://codepen.io/sosuke/pen/Pjoqqp) to convert hex colors to CSS filters. + +--- + +## Responsive Design + +### Breakpoints + +```css +/* Mobile First Approach */ + +/* Extra small devices (phones, less than 576px) */ +/* Default styles here */ + +/* Small devices (landscape phones, 576px and up) */ +@media (min-width: 576px) { } + +/* Medium devices (tablets, 768px and up) */ +@media (min-width: 768px) { } + +/* Large devices (desktops, 992px and up) */ +@media (min-width: 992px) { } + +/* Extra large devices (large desktops, 1200px and up) */ +@media (min-width: 1200px) { } +``` + +### Mobile Adjustments + +**Language Selector:** +```css +@media (max-width: 576px) { + .custom-select { + font-size: 0.9rem; + padding: 0.5rem; + } +} +``` + +**Typography:** +```css +@media (max-width: 992px) { + .display-5 { + font-size: 2.5rem; /* Smaller on mobile */ + } +} +``` + +--- + +## Animations + +### Transitions + +**Standard transition:** +```css +.element { + transition: all 0.3s ease; +} +``` + +**Specific properties:** +```css +.button { + transition: transform 0.2s ease, + background-color 0.3s ease; +} +``` + +### Hover Effects + +**Lift effect:** +```css +.card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); +} +``` + +**Scale effect:** +```css +.icon:hover { + transform: scale(1.1); +} +``` + +--- + +## Accessibility + +### Color Contrast + +All color combinations meet **WCAG AA** standards: +- Navy text on white: 14.96:1 ✅ +- White text on navy: 14.96:1 ✅ +- Blue text on white: 8.59:1 ✅ + +### Focus States + +```css +.focusable:focus { + outline: 2px solid var(--clr-blue-text); + outline-offset: 2px; +} +``` + +### Screen Reader Support + +```html + +GitHub profile link + + + + + +