diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4feaae7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,34 @@ +version: 2 +updates: + # Enable version updates for npm + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'weekly' + day: 'monday' + open-pull-requests-limit: 10 + groups: + angular: + patterns: + - '@angular/*' + development-dependencies: + dependency-type: 'development' + update-types: + - 'minor' + - 'patch' + + # Enable version updates for GitHub Actions + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' + day: 'monday' + open-pull-requests-limit: 5 + + # Enable version updates for Docker + - package-ecosystem: 'docker' + directory: '/' + schedule: + interval: 'weekly' + day: 'monday' + open-pull-requests-limit: 5 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 333380b..263a90f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,101 +1,41 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL Advanced" +name: CodeQL Security Analysis on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - schedule: - - cron: '27 7 * * 2' + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '0 0 * * 1' # Run every Monday at midnight UTC jobs: - analyze: - name: Analyze (${{ matrix.language }}) - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners (GitHub.com only) - # Consider using larger runners or machines with greater resources for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} - permissions: - # required for all workflows - security-events: write + analyze: + name: Analyze Code + runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + contents: read - # required to fetch internal or private CodeQL packs - packages: read + strategy: + fail-fast: false + matrix: + language: ['javascript-typescript'] - # only required for workflows in private repositories - actions: read - contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 - strategy: - fail-fast: false - matrix: - include: - - language: actions - build-mode: none - - language: javascript-typescript - build-mode: none - # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' - # Use `c-cpp` to analyze code written in C, C++ or both - # Use 'java-kotlin' to analyze code written in Java, Kotlin or both - # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, - # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. - # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how - # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages - steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-extended,security-and-quality - # Add any setup steps before running the `github/codeql-action/init` action. - # This includes steps like installing compilers or runtimes (`actions/setup-node` - # or others). This is typically only required for manual builds. - # - name: Setup runtime (example) - # uses: actions/setup-example@v1 + - name: Autobuild + uses: github/codeql-action/autobuild@v3 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - name: Run manual build steps - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: '/language:${{matrix.language}}' diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 2fab822..72d6151 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -5,6 +5,7 @@ on: branches: [main] release: types: [published] + workflow_dispatch: jobs: build-and-push: @@ -12,10 +13,14 @@ jobs: permissions: contents: read packages: write + security-events: write steps: - uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: @@ -33,11 +38,27 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha + type=raw,value=latest,enable={{is_default_branch}} - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.27.0 + with: + image-ref: ghcr.io/${{ github.repository }}:sha-${{ github.sha }} + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml new file mode 100644 index 0000000..6104ec2 --- /dev/null +++ b/.github/workflows/pr-validation.yml @@ -0,0 +1,105 @@ +name: PR Validation + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + lint: + name: Lint Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Check code formatting with Prettier + run: npx prettier --check "src/**/*.{ts,html,scss,css,json}" + + - name: Run ESLint + run: npx eslint "src/**/*.ts" + + type-check: + name: TypeScript Type Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run TypeScript compiler + run: npx tsc --noEmit + + test: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm test -- --watch=false --browsers=ChromeHeadless --code-coverage + + - name: Upload coverage reports + uses: codecov/codecov-action@v4 + if: always() + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/vac-optimizer/lcov.info + fail_ci_if_error: false + + build: + name: Build Application + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build production bundle + run: npm run build -- --configuration production + env: + NG_BUILD_CACHE: '.angular/cache' + + - name: Check bundle size + run: | + du -sh dist/vac-optimizer/browser + echo "Build completed successfully" + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: dist/ + retention-days: 7 diff --git a/README.md b/README.md index 1c12c95..9dc8f9f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,38 @@ ng serve Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. +## Code Quality + +This project uses ESLint and Prettier to maintain code quality and consistency. + +### Linting + +Check for linting errors: + +```bash +npm run lint +``` + +Auto-fix linting issues: + +```bash +npm run lint:fix +``` + +### Formatting + +Check code formatting: + +```bash +npm run format:check +``` + +Format code: + +```bash +npm run format +``` + ## Code scaffolding Angular CLI includes powerful code scaffolding tools. To generate a new component, run: @@ -54,6 +86,15 @@ ng e2e Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. +## CI/CD + +This project uses GitHub Actions for continuous integration and deployment: + +- **PR Validation**: Automatically runs on pull requests to validate code quality, run tests, and build the application +- **Docker Build & Push**: Builds and publishes Docker images to GitHub Container Registry on main branch pushes and releases +- **CodeQL Analysis**: Runs security analysis weekly and on pull requests +- **Dependabot**: Automatically updates dependencies weekly + ## Additional Resources For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/angular.json b/angular.json index 643f287..cc88982 100644 --- a/angular.json +++ b/angular.json @@ -52,7 +52,10 @@ "maximumError": "16kB" } ], - "outputHashing": "all" + "outputHashing": "all", + "optimization": { + "fonts": false + } }, "development": { "optimization": false, diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..7f7817d --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,34 @@ +import eslintJs from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import prettier from 'eslint-config-prettier'; +import prettierPlugin from 'eslint-plugin-prettier'; + +export default [ + eslintJs.configs.recommended, + ...tseslint.configs.recommended, + prettier, + { + files: ['**/*.ts'], + plugins: { + prettier: prettierPlugin, + }, + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + 'prettier/prettier': 'error', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + }, + }, + { + ignores: ['dist/**', 'node_modules/**', '.angular/**', 'coverage/**'], + }, +]; diff --git a/package-lock.json b/package-lock.json index a8d29ec..c23ee2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@angular/build": "^20.3.7", "@angular/cli": "^20.3.7", "@angular/compiler-cli": "^20.3.0", + "@eslint/js": "^9.17.0", "@types/express": "^5.0.1", "@types/jasmine": "~5.1.0", "@types/jest": "^30.0.0", @@ -44,7 +45,8 @@ "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "prettier": "^3.6.2", - "typescript": "~5.9.2" + "typescript": "~5.9.2", + "typescript-eslint": "^8.19.1" } }, "node_modules/@acemir/cssom": { @@ -3718,6 +3720,44 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@npmcli/agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", @@ -5123,6 +5163,264 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", + "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/type-utils": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.3", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", + "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", + "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.3", + "@typescript-eslint/types": "^8.46.3", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", + "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", + "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", + "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", + "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", + "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.3", + "@typescript-eslint/tsconfig-utils": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", + "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", + "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -7680,6 +7978,36 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "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", @@ -7711,6 +8039,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -8127,6 +8465,13 @@ "dev": true, "license": "ISC" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -10696,6 +11041,16 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -12251,6 +12606,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -12427,6 +12803,17 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -12531,6 +12918,30 @@ "node": ">= 18" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -13695,6 +14106,19 @@ "node": ">=6" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-jest": { "version": "29.4.5", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", @@ -13859,6 +14283,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.3.tgz", + "integrity": "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.3", + "@typescript-eslint/parser": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/ua-parser-js": { "version": "0.7.41", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", diff --git a/package.json b/package.json index 74ff98f..7da0bdc 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,11 @@ "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", - "serve:ssr:vac-optimizer": "node dist/vac-optimizer/server/server.mjs" + "serve:ssr:vac-optimizer": "node dist/vac-optimizer/server/server.mjs", + "lint": "eslint \"src/**/*.ts\"", + "lint:fix": "eslint \"src/**/*.ts\" --fix", + "format": "prettier --write \"src/**/*.{ts,html,scss,css,json}\"", + "format:check": "prettier --check \"src/**/*.{ts,html,scss,css,json}\"" }, "prettier": { "printWidth": 100, @@ -43,6 +47,7 @@ "@angular/build": "^20.3.7", "@angular/cli": "^20.3.7", "@angular/compiler-cli": "^20.3.0", + "@eslint/js": "^9.17.0", "@types/express": "^5.0.1", "@types/jasmine": "~5.1.0", "@types/jest": "^30.0.0", @@ -59,6 +64,7 @@ "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "prettier": "^3.6.2", - "typescript": "~5.9.2" + "typescript": "~5.9.2", + "typescript-eslint": "^8.19.1" } } diff --git a/src/app/app.config.server.ts b/src/app/app.config.server.ts index a5585da..ce0d24f 100644 --- a/src/app/app.config.server.ts +++ b/src/app/app.config.server.ts @@ -4,9 +4,7 @@ import { appConfig } from './app.config'; import { serverRoutes } from './app.routes.server'; const serverConfig: ApplicationConfig = { - providers: [ - provideServerRendering(withRoutes(serverRoutes)) - ] + providers: [provideServerRendering(withRoutes(serverRoutes))], }; export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts index a358ea0..abf676c 100644 --- a/src/app/app.routes.server.ts +++ b/src/app/app.routes.server.ts @@ -3,6 +3,6 @@ import { RenderMode, ServerRoute } from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { path: '**', - renderMode: RenderMode.Client - } + renderMode: RenderMode.Client, + }, ]; diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts index 3aaada2..bb852d2 100644 --- a/src/app/app.spec.ts +++ b/src/app/app.spec.ts @@ -6,7 +6,7 @@ describe('App', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [App], - providers: [provideZonelessChangeDetection()] + providers: [provideZonelessChangeDetection()], }).compileComponents(); }); diff --git a/src/app/app.ts b/src/app/app.ts index 835574a..5e97ef1 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, model, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { provideNativeDateAdapter } from '@angular/material/core'; import { MatDivider } from '@angular/material/divider'; diff --git a/src/app/components/calendar-month/calendar-month.spec.ts b/src/app/components/calendar-month/calendar-month.spec.ts index 4854521..b74c8c5 100644 --- a/src/app/components/calendar-month/calendar-month.spec.ts +++ b/src/app/components/calendar-month/calendar-month.spec.ts @@ -3,22 +3,21 @@ import { provideZonelessChangeDetection } from '@angular/core'; import { CalendarMonth } from './calendar-month'; describe('CalendarMonth', () => { - let component: CalendarMonth; - let fixture: ComponentFixture; + let component: CalendarMonth; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CalendarMonth], - providers: [provideZonelessChangeDetection()] - }) - .compileComponents(); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CalendarMonth], + providers: [provideZonelessChangeDetection()], + }).compileComponents(); - fixture = TestBed.createComponent(CalendarMonth); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + fixture = TestBed.createComponent(CalendarMonth); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); }); diff --git a/src/app/components/calendar-settings/calendar-settings.spec.ts b/src/app/components/calendar-settings/calendar-settings.spec.ts index 0fbfe5e..35b5cce 100644 --- a/src/app/components/calendar-settings/calendar-settings.spec.ts +++ b/src/app/components/calendar-settings/calendar-settings.spec.ts @@ -3,22 +3,21 @@ import { provideZonelessChangeDetection } from '@angular/core'; import { CalendarSettings } from './calendar-settings'; describe('CalendarSettings', () => { - let component: CalendarSettings; - let fixture: ComponentFixture; + let component: CalendarSettings; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CalendarSettings], - providers: [provideZonelessChangeDetection()] - }) - .compileComponents(); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CalendarSettings], + providers: [provideZonelessChangeDetection()], + }).compileComponents(); - fixture = TestBed.createComponent(CalendarSettings); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + fixture = TestBed.createComponent(CalendarSettings); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); }); diff --git a/src/app/components/calendar-settings/calendar-settings.ts b/src/app/components/calendar-settings/calendar-settings.ts index 9d95a49..6bdd029 100644 --- a/src/app/components/calendar-settings/calendar-settings.ts +++ b/src/app/components/calendar-settings/calendar-settings.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; -import { MatButton, MatButtonModule } from '@angular/material/button'; +import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; -import { MatIcon, MatIconModule } from '@angular/material/icon'; +import { MatIconModule } from '@angular/material/icon'; import { CalendarSettingsDialog } from '../dialog/calendar-settings-dialog/calendar-settings-dialog'; import { MatDividerModule } from '@angular/material/divider'; diff --git a/src/app/components/calendar-year/calendar-year.html b/src/app/components/calendar-year/calendar-year.html index 28d6203..6d75550 100644 --- a/src/app/components/calendar-year/calendar-year.html +++ b/src/app/components/calendar-year/calendar-year.html @@ -1,17 +1,21 @@
@if (isLoading()) { -
-
- -

Calcul en cours...

+
+
+ +

Calcul en cours...

+
-
} @for (month of months; track $index) { - - - - - + + + + + }
diff --git a/src/app/components/calendar-year/calendar-year.scss b/src/app/components/calendar-year/calendar-year.scss index 47b8ecf..2309a59 100644 --- a/src/app/components/calendar-year/calendar-year.scss +++ b/src/app/components/calendar-year/calendar-year.scss @@ -66,5 +66,10 @@ } :root { - @include mat.progress-spinner-overrides((size: 0.5rem, active-indicator-width: 0.25rem)); + @include mat.progress-spinner-overrides( + ( + size: 0.5rem, + active-indicator-width: 0.25rem, + ) + ); } diff --git a/src/app/components/calendar-year/calendar-year.spec.ts b/src/app/components/calendar-year/calendar-year.spec.ts index 6a027bf..55b76c5 100644 --- a/src/app/components/calendar-year/calendar-year.spec.ts +++ b/src/app/components/calendar-year/calendar-year.spec.ts @@ -3,22 +3,21 @@ import { provideZonelessChangeDetection } from '@angular/core'; import { CalendarYear } from './calendar-year'; describe('CalendarYear', () => { - let component: CalendarYear; - let fixture: ComponentFixture; + let component: CalendarYear; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CalendarYear], - providers: [provideZonelessChangeDetection()] - }) - .compileComponents(); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CalendarYear], + providers: [provideZonelessChangeDetection()], + }).compileComponents(); - fixture = TestBed.createComponent(CalendarYear); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + fixture = TestBed.createComponent(CalendarYear); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); }); diff --git a/src/app/components/dialog/calendar-settings-dialog/calendar-settings-dialog.spec.ts b/src/app/components/dialog/calendar-settings-dialog/calendar-settings-dialog.spec.ts index 402949c..2a6de50 100644 --- a/src/app/components/dialog/calendar-settings-dialog/calendar-settings-dialog.spec.ts +++ b/src/app/components/dialog/calendar-settings-dialog/calendar-settings-dialog.spec.ts @@ -4,39 +4,38 @@ import { MatDialogRef } from '@angular/material/dialog'; import { CalendarSettingsDialog } from './calendar-settings-dialog'; describe('CalendarSettingsDialog', () => { - let component: CalendarSettingsDialog; - let fixture: ComponentFixture; + let component: CalendarSettingsDialog; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CalendarSettingsDialog], - providers: [ - provideZonelessChangeDetection(), - { - provide: MatDialogRef, - useValue: { - close: jasmine.createSpy('close') - } - } - ] - }) - .compileComponents(); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CalendarSettingsDialog], + providers: [ + provideZonelessChangeDetection(), + { + provide: MatDialogRef, + useValue: { + close: jasmine.createSpy('close'), + }, + }, + ], + }).compileComponents(); - fixture = TestBed.createComponent(CalendarSettingsDialog); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + fixture = TestBed.createComponent(CalendarSettingsDialog); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); - it('should have calendar settings service', () => { - expect(component['calendarSettingsService']).toBeDefined(); - }); + it('should have calendar settings service', () => { + expect(component['calendarSettingsService']).toBeDefined(); + }); - it('should close dialog when closeDialog is called', () => { - component.closeDialog(); - expect(component.dialogRef.close).toHaveBeenCalled(); - }); + it('should close dialog when closeDialog is called', () => { + component.closeDialog(); + expect(component.dialogRef.close).toHaveBeenCalled(); + }); }); diff --git a/src/app/components/dialog/calendar-settings-dialog/calendar-settings-dialog.ts b/src/app/components/dialog/calendar-settings-dialog/calendar-settings-dialog.ts index 24255e5..b2657fd 100644 --- a/src/app/components/dialog/calendar-settings-dialog/calendar-settings-dialog.ts +++ b/src/app/components/dialog/calendar-settings-dialog/calendar-settings-dialog.ts @@ -1,10 +1,8 @@ import { Component } from '@angular/core'; import { MatButton } from '@angular/material/button'; -import { MatCheckbox } from '@angular/material/checkbox'; import { MatDialogRef } from '@angular/material/dialog'; import { CalendarSettingsService } from '../../../services/calendar-settings-service'; import { MatSlideToggle } from '@angular/material/slide-toggle'; -import { CdkFixedSizeVirtualScroll } from '@angular/cdk/scrolling'; @Component({ selector: 'app-calendar-settings-dialog', diff --git a/src/app/components/user-input/user-input.spec.ts b/src/app/components/user-input/user-input.spec.ts index 2de59e0..11c67b2 100644 --- a/src/app/components/user-input/user-input.spec.ts +++ b/src/app/components/user-input/user-input.spec.ts @@ -3,39 +3,38 @@ import { provideZonelessChangeDetection } from '@angular/core'; import { UserInput } from './user-input'; describe('UserInput', () => { - let component: UserInput; - let fixture: ComponentFixture; + let component: UserInput; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [UserInput], - providers: [provideZonelessChangeDetection()] - }) - .compileComponents(); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserInput], + providers: [provideZonelessChangeDetection()], + }).compileComponents(); - fixture = TestBed.createComponent(UserInput); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + fixture = TestBed.createComponent(UserInput); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); - it('should initialize form with values from service', () => { - expect(component.userInputForm).toBeDefined(); - expect(component.userInputForm.get('CP')).toBeDefined(); - expect(component.userInputForm.get('RTT')).toBeDefined(); - expect(component.userInputForm.get('Others')).toBeDefined(); - }); + it('should initialize form with values from service', () => { + expect(component.userInputForm).toBeDefined(); + expect(component.userInputForm.get('CP')).toBeDefined(); + expect(component.userInputForm.get('RTT')).toBeDefined(); + expect(component.userInputForm.get('Others')).toBeDefined(); + }); - it('should have valid form controls', () => { - const cpControl = component.userInputForm.get('CP'); - const rttControl = component.userInputForm.get('RTT'); - const othersControl = component.userInputForm.get('Others'); + it('should have valid form controls', () => { + const cpControl = component.userInputForm.get('CP'); + const rttControl = component.userInputForm.get('RTT'); + const othersControl = component.userInputForm.get('Others'); - expect(cpControl).toBeTruthy(); - expect(rttControl).toBeTruthy(); - expect(othersControl).toBeTruthy(); - }); + expect(cpControl).toBeTruthy(); + expect(rttControl).toBeTruthy(); + expect(othersControl).toBeTruthy(); + }); }); diff --git a/src/app/components/user-input/user-input.ts b/src/app/components/user-input/user-input.ts index c217c0a..dd29741 100644 --- a/src/app/components/user-input/user-input.ts +++ b/src/app/components/user-input/user-input.ts @@ -1,7 +1,7 @@ import { Component, OnInit, OnDestroy, effect } from '@angular/core'; import { MatFormField, MatLabel } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { UserInputService } from '../../services/user-input-service'; import { VacationsNumber } from '../../services/calendar-service'; import { debounceTime, takeUntil, Subject } from 'rxjs'; diff --git a/src/app/components/user-settings/user-settings.spec.ts b/src/app/components/user-settings/user-settings.spec.ts index d0a0eaa..7a2fea9 100644 --- a/src/app/components/user-settings/user-settings.spec.ts +++ b/src/app/components/user-settings/user-settings.spec.ts @@ -3,22 +3,21 @@ import { provideZonelessChangeDetection } from '@angular/core'; import { UserSettings } from './user-settings'; describe('UserSettings', () => { - let component: UserSettings; - let fixture: ComponentFixture; + let component: UserSettings; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [UserSettings], - providers: [provideZonelessChangeDetection()] - }) - .compileComponents(); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserSettings], + providers: [provideZonelessChangeDetection()], + }).compileComponents(); - fixture = TestBed.createComponent(UserSettings); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + fixture = TestBed.createComponent(UserSettings); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); }); diff --git a/src/app/services/calendar-service.spec.ts b/src/app/services/calendar-service.spec.ts index 48aa9c4..5065932 100644 --- a/src/app/services/calendar-service.spec.ts +++ b/src/app/services/calendar-service.spec.ts @@ -1,11 +1,6 @@ import { TestBed } from '@angular/core/testing'; import { provideZonelessChangeDetection } from '@angular/core'; -import { - CalendarService, - DayType, - SelectedDate, - SelectedDates, -} from './calendar-service'; +import { CalendarService, DayType, SelectedDate, SelectedDates } from './calendar-service'; import { DateRange } from '@angular/material/datepicker'; describe('CalendarService', () => { @@ -111,47 +106,32 @@ describe('CalendarService', () => { describe('SelectedDate', () => { it('should check if date is selected', () => { const date = new Date(2024, 0, 1); - const selectedDate = new SelectedDate( - DayType.CP, - new DateRange(date, date), - ); + const selectedDate = new SelectedDate(DayType.CP, new DateRange(date, date)); expect(selectedDate.isSelected(date)).toBe(true); }); it('should check if date is start', () => { const date = new Date(2024, 0, 1); - const selectedDate = new SelectedDate( - DayType.CP, - new DateRange(date, date), - ); + const selectedDate = new SelectedDate(DayType.CP, new DateRange(date, date)); expect(selectedDate.isStart(date)).toBe(true); }); it('should check if date is end', () => { const date = new Date(2024, 0, 1); - const selectedDate = new SelectedDate( - DayType.CP, - new DateRange(date, date), - ); + const selectedDate = new SelectedDate(DayType.CP, new DateRange(date, date)); expect(selectedDate.isEnd(date)).toBe(true); }); it('should return correct classes for date', () => { const date = new Date(2024, 0, 1); - const selectedDate = new SelectedDate( - DayType.CP, - new DateRange(date, date), - ); + const selectedDate = new SelectedDate(DayType.CP, new DateRange(date, date)); const classes = selectedDate.getClasses(date); expect(classes).toContain(DayType.CP); }); it('should return tooltip for selected date', () => { const date = new Date(2024, 0, 1); - const selectedDate = new SelectedDate( - DayType.CP, - new DateRange(date, date), - ); + const selectedDate = new SelectedDate(DayType.CP, new DateRange(date, date)); const tooltip = selectedDate.getTooltip(date); expect(tooltip).toBeTruthy(); }); @@ -159,19 +139,13 @@ describe('SelectedDate', () => { it('should calculate length in days', () => { const startDate = new Date(2024, 0, 1); const endDate = new Date(2024, 0, 3); - const selectedDate = new SelectedDate( - DayType.CP, - new DateRange(startDate, endDate), - ); + const selectedDate = new SelectedDate(DayType.CP, new DateRange(startDate, endDate)); expect(selectedDate.getLengthInDays()).toBe(3); }); it('should not mark TODAY as selected', () => { const date = new Date(2024, 0, 1); - const selectedDate = new SelectedDate( - DayType.TODAY, - new DateRange(date, date), - ); + const selectedDate = new SelectedDate(DayType.TODAY, new DateRange(date, date)); expect(selectedDate.isSelected(date)).toBe(false); }); }); @@ -180,18 +154,14 @@ describe('SelectedDates', () => { it('should push selected dates', () => { const selectedDates = new SelectedDates(); const date = new Date(2024, 0, 1); - selectedDates.push( - new SelectedDate(DayType.CP, new DateRange(date, date)), - ); + selectedDates.push(new SelectedDate(DayType.CP, new DateRange(date, date))); expect(selectedDates._datesSelected.length).toBe(1); }); it('should check if date is selected', () => { const selectedDates = new SelectedDates(); const date = new Date(2024, 0, 1); - selectedDates.push( - new SelectedDate(DayType.CP, new DateRange(date, date)), - ); + selectedDates.push(new SelectedDate(DayType.CP, new DateRange(date, date))); expect(selectedDates.isSelected(date)).toBe(true); }); @@ -212,9 +182,7 @@ describe('SelectedDates', () => { it('should compute heuristics', () => { const selectedDates = new SelectedDates(); const date = new Date(2024, 0, 1); - selectedDates.push( - new SelectedDate(DayType.CP, new DateRange(date, date)), - ); + selectedDates.push(new SelectedDate(DayType.CP, new DateRange(date, date))); const heuristic = selectedDates.computeHeuristics(); expect(typeof heuristic).toBe('number'); }); @@ -222,9 +190,7 @@ describe('SelectedDates', () => { it('should return correct classes', () => { const selectedDates = new SelectedDates(); const date = new Date(2024, 0, 1); - selectedDates.push( - new SelectedDate(DayType.CP, new DateRange(date, date)), - ); + selectedDates.push(new SelectedDate(DayType.CP, new DateRange(date, date))); const classes = selectedDates.getClasses(date); expect(classes).toContain(DayType.CP); }); @@ -232,9 +198,7 @@ describe('SelectedDates', () => { it('should return tooltip', () => { const selectedDates = new SelectedDates(); const date = new Date(2024, 0, 1); - selectedDates.push( - new SelectedDate(DayType.CP, new DateRange(date, date)), - ); + selectedDates.push(new SelectedDate(DayType.CP, new DateRange(date, date))); const tooltip = selectedDates.getTooltip(date); expect(tooltip).toBeTruthy(); }); diff --git a/src/app/services/calendar-service.ts b/src/app/services/calendar-service.ts index 8edf06d..8c1d2a3 100644 --- a/src/app/services/calendar-service.ts +++ b/src/app/services/calendar-service.ts @@ -28,8 +28,8 @@ export class CalendarService { // Get selected dates from current date to the next year getSelectedDatesFromNow(): SelectedDates { - const holidays = this.getHolidays(); - let selectedDates: SelectedDates = new SelectedDates(); + const _holidays = this.getHolidays(); + const selectedDates: SelectedDates = new SelectedDates(); // Logic to populate selectedDates from now until the next year return selectedDates; } @@ -37,7 +37,7 @@ export class CalendarService { getSelectedDatesForYear(): SelectedDates { const holidays = this.getHolidays(); const monthInAYear = this.monthsInAYearFromNow(); - let selectedDates: SelectedDates = new SelectedDates(); + const selectedDates: SelectedDates = new SelectedDates(); // Use DateCacheService to get today's date components const { @@ -519,7 +519,7 @@ export class SelectedDates implements SelectedDateInterface { const daysInRange = selectedDate.getLengthInDays(); // For each day in the range, add it to the appropriate month - let currentDate = new Date(selectedDate.range.start); + const currentDate = new Date(selectedDate.range.start); for (let i = 0; i < daysInRange; i++) { const monthKey = currentDate.getFullYear() * 12 + currentDate.getMonth(); monthCounts.set(monthKey, (monthCounts.get(monthKey) ?? 0) + 1); @@ -719,7 +719,10 @@ export class SelectedDates implements SelectedDateInterface { } } - lookForVacation(vacType: DayType, calendarService: CalendarService): { apply: () => void; heuristic: number } { + lookForVacation( + vacType: DayType, + calendarService: CalendarService, + ): { apply: () => void; heuristic: number } { const daysOfYear = calendarService.monthsInAYearFromNow(); const now = new Date(); now.setHours(0, 0, 0, 0); diff --git a/src/app/services/theme-service.spec.ts b/src/app/services/theme-service.spec.ts index aca0ffa..f21cfb1 100644 --- a/src/app/services/theme-service.spec.ts +++ b/src/app/services/theme-service.spec.ts @@ -3,40 +3,40 @@ import { provideZonelessChangeDetection } from '@angular/core'; import { ThemeService } from './theme-service'; describe('ThemeService', () => { - let service: ThemeService; + let service: ThemeService; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideZonelessChangeDetection()], - }); - service = TestBed.inject(ThemeService); - }); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideZonelessChangeDetection()], + }); + service = TestBed.inject(ThemeService); + }); - it('should be created', () => { - expect(service).toBeTruthy(); - }); + it('should be created', () => { + expect(service).toBeTruthy(); + }); - it('should initialize with system preference', () => { - expect(service.isLightMode()).toBeDefined(); - }); + it('should initialize with system preference', () => { + expect(service.isLightMode()).toBeDefined(); + }); - it('should toggle theme', () => { - const initialMode = service.isLightMode(); - service.toggleTheme(); - expect(service.isLightMode()).toBe(!initialMode); - }); + it('should toggle theme', () => { + const initialMode = service.isLightMode(); + service.toggleTheme(); + expect(service.isLightMode()).toBe(!initialMode); + }); - it('should force light mode when specified', () => { - service.toggleTheme(true); - expect(service.isLightMode()).toBe(true); - expect(document.body.classList.contains('light-mode')).toBe(true); - expect(document.body.classList.contains('dark-mode')).toBe(false); - }); + it('should force light mode when specified', () => { + service.toggleTheme(true); + expect(service.isLightMode()).toBe(true); + expect(document.body.classList.contains('light-mode')).toBe(true); + expect(document.body.classList.contains('dark-mode')).toBe(false); + }); - it('should force dark mode when specified', () => { - service.toggleTheme(false); - expect(service.isLightMode()).toBe(false); - expect(document.body.classList.contains('light-mode')).toBe(false); - expect(document.body.classList.contains('dark-mode')).toBe(true); - }); + it('should force dark mode when specified', () => { + service.toggleTheme(false); + expect(service.isLightMode()).toBe(false); + expect(document.body.classList.contains('light-mode')).toBe(false); + expect(document.body.classList.contains('dark-mode')).toBe(true); + }); }); diff --git a/src/app/services/user-input-service.ts b/src/app/services/user-input-service.ts index faf9eea..d9aa2f8 100644 --- a/src/app/services/user-input-service.ts +++ b/src/app/services/user-input-service.ts @@ -1,4 +1,4 @@ -import { Injectable, signal, PLATFORM_ID, inject } from '@angular/core'; +import { Injectable, signal } from '@angular/core'; import { VacationsNumber } from './calendar-service'; @Injectable({ diff --git a/src/index.html b/src/index.html index 8f1ea4b..52a8f31 100644 --- a/src/index.html +++ b/src/index.html @@ -1,18 +1,19 @@ - - + VacOptimizer - - - - - + + + + + - - \ No newline at end of file + diff --git a/src/main.server.ts b/src/main.server.ts index 35f65f2..9b1aba7 100644 --- a/src/main.server.ts +++ b/src/main.server.ts @@ -2,7 +2,6 @@ import { BootstrapContext, bootstrapApplication } from '@angular/platform-browse import { App } from './app/app'; import { config } from './app/app.config.server'; -const bootstrap = (context: BootstrapContext) => - bootstrapApplication(App, config, context); +const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, config, context); export default bootstrap; diff --git a/src/main.ts b/src/main.ts index 54da693..190f341 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,5 +2,4 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { App } from './app/app'; -bootstrapApplication(App, appConfig) - .catch((err) => console.error(err)); +bootstrapApplication(App, appConfig).catch((err) => console.error(err)); diff --git a/src/server.ts b/src/server.ts index 92c1e9c..7611941 100644 --- a/src/server.ts +++ b/src/server.ts @@ -41,9 +41,7 @@ app.use( app.use((req, res, next) => { angularApp .handle(req) - .then((response) => - response ? writeResponseToNodeResponse(response, res) : next(), - ) + .then((response) => (response ? writeResponseToNodeResponse(response, res) : next())) .catch(next); });