From cea30418fa8d7e7749aa2e755e18f99ec6950be8 Mon Sep 17 00:00:00 2001 From: Fernando Tona Date: Sun, 5 Oct 2025 02:58:00 +0100 Subject: [PATCH 1/2] 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 2/2] 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