test #1
enterprise-version into main.github/workflows/ci.yml +88 -0 | new file mode 100644 | ||
| @@ -0,0 +1,88 @@ | ||
| 1 | +name: CI | |
| 2 | + | |
| 3 | +on: | |
| 4 | + push: | |
| 5 | + branches: [enterprise-version] | |
| 6 | + pull_request: | |
| 7 | + | |
| 8 | +jobs: | |
| 9 | + build: | |
| 10 | + name: Build | |
| 11 | + runs-on: ubuntu-latest | |
| 12 | + steps: | |
| 13 | + - name: Checkout road-trip | |
| 14 | + uses: actions/checkout@v4 | |
| 15 | + with: | |
| 16 | + path: roadTrip | |
| 17 | + | |
| 18 | + # road-trip depends on ../ui-kit/dist/ui-kit (pnpm link) | |
| 19 | + - name: Checkout ui-kit | |
| 20 | + env: | |
| 21 | + UIKIT_REPO_URL: https://rickub.com/yatho/demo-angular-migration-uikit.git | |
| 22 | + run: git clone --depth 1 "$UIKIT_REPO_URL" ui-kit | |
| 23 | + | |
| 24 | + - uses: pnpm/action-setup@v4 | |
| 25 | + with: | |
| 26 | + version: 9 | |
| 27 | + | |
| 28 | + - uses: actions/setup-node@v4 | |
| 29 | + with: | |
| 30 | + node-version: 20 | |
| 31 | + cache: pnpm | |
| 32 | + cache-dependency-path: | | |
| 33 | + roadTrip/pnpm-lock.yaml | |
| 34 | + ui-kit/pnpm-lock.yaml | |
| 35 | + | |
| 36 | + - name: Build ui-kit | |
| 37 | + working-directory: ui-kit | |
| 38 | + run: | | |
| 39 | + pnpm install --frozen-lockfile | |
| 40 | + pnpm build | |
| 41 | + | |
| 42 | + - name: Install road-trip | |
| 43 | + working-directory: roadTrip | |
| 44 | + run: pnpm install --frozen-lockfile | |
| 45 | + | |
| 46 | + - name: Build road-trip | |
| 47 | + working-directory: roadTrip | |
| 48 | + run: pnpm build | |
| 49 | + | |
| 50 | + test: | |
| 51 | + name: Test | |
| 52 | + runs-on: ubuntu-latest | |
| 53 | + steps: | |
| 54 | + - name: Checkout road-trip | |
| 55 | + uses: actions/checkout@v4 | |
| 56 | + with: | |
| 57 | + path: roadTrip | |
| 58 | + | |
| 59 | + - name: Checkout ui-kit | |
| 60 | + env: | |
| 61 | + UIKIT_REPO_URL: https://rickub.com/yatho/demo-angular-migration-uikit.git | |
| 62 | + run: git clone --depth 1 "$UIKIT_REPO_URL" ui-kit | |
| 63 | + | |
| 64 | + - uses: pnpm/action-setup@v4 | |
| 65 | + with: | |
| 66 | + version: 9 | |
| 67 | + | |
| 68 | + - uses: actions/setup-node@v4 | |
| 69 | + with: | |
| 70 | + node-version: 20 | |
| 71 | + cache: pnpm | |
| 72 | + cache-dependency-path: | | |
| 73 | + roadTrip/pnpm-lock.yaml | |
| 74 | + ui-kit/pnpm-lock.yaml | |
| 75 | + | |
| 76 | + - name: Build ui-kit | |
| 77 | + working-directory: ui-kit | |
| 78 | + run: | | |
| 79 | + pnpm install --frozen-lockfile | |
| 80 | + pnpm build | |
| 81 | + | |
| 82 | + - name: Install road-trip | |
| 83 | + working-directory: roadTrip | |
| 84 | + run: pnpm install --frozen-lockfile | |
| 85 | + | |
| 86 | + - name: Test road-trip | |
| 87 | + working-directory: roadTrip | |
| 88 | + run: pnpm test --no-watch --karma-config=karma.ci.conf.js | |
| new file mode 100644 | |||
| @@ -0,0 +1,88 @@ | |||
| 1 | +name: CI | ||
| 2 | + | ||
| 3 | +on: | ||
| 4 | + push: | ||
| 5 | + branches: [enterprise-version] | ||
| 6 | + pull_request: | ||
| 7 | + | ||
| 8 | +jobs: | ||
| 9 | + build: | ||
| 10 | + name: Build | ||
| 11 | + runs-on: ubuntu-latest | ||
| 12 | + steps: | ||
| 13 | + - name: Checkout road-trip | ||
| 14 | + uses: actions/checkout@v4 | ||
| 15 | + with: | ||
| 16 | + path: roadTrip | ||
| 17 | + | ||
| 18 | + # road-trip depends on ../ui-kit/dist/ui-kit (pnpm link) | ||
| 19 | + - name: Checkout ui-kit | ||
| 20 | + env: | ||
| 21 | + UIKIT_REPO_URL: https://rickub.com/yatho/demo-angular-migration-uikit.git | ||
| 22 | + run: git clone --depth 1 "$UIKIT_REPO_URL" ui-kit | ||
| 23 | + | ||
| 24 | + - uses: pnpm/action-setup@v4 | ||
| 25 | + with: | ||
| 26 | + version: 9 | ||
| 27 | + | ||
| 28 | + - uses: actions/setup-node@v4 | ||
| 29 | + with: | ||
| 30 | + node-version: 20 | ||
| 31 | + cache: pnpm | ||
| 32 | + cache-dependency-path: | | ||
| 33 | + roadTrip/pnpm-lock.yaml | ||
| 34 | + ui-kit/pnpm-lock.yaml | ||
| 35 | + | ||
| 36 | + - name: Build ui-kit | ||
| 37 | + working-directory: ui-kit | ||
| 38 | + run: | | ||
| 39 | + pnpm install --frozen-lockfile | ||
| 40 | + pnpm build | ||
| 41 | + | ||
| 42 | + - name: Install road-trip | ||
| 43 | + working-directory: roadTrip | ||
| 44 | + run: pnpm install --frozen-lockfile | ||
| 45 | + | ||
| 46 | + - name: Build road-trip | ||
| 47 | + working-directory: roadTrip | ||
| 48 | + run: pnpm build | ||
| 49 | + | ||
| 50 | + test: | ||
| 51 | + name: Test | ||
| 52 | + runs-on: ubuntu-latest | ||
| 53 | + steps: | ||
| 54 | + - name: Checkout road-trip | ||
| 55 | + uses: actions/checkout@v4 | ||
| 56 | + with: | ||
| 57 | + path: roadTrip | ||
| 58 | + | ||
| 59 | + - name: Checkout ui-kit | ||
| 60 | + env: | ||
| 61 | + UIKIT_REPO_URL: https://rickub.com/yatho/demo-angular-migration-uikit.git | ||
| 62 | + run: git clone --depth 1 "$UIKIT_REPO_URL" ui-kit | ||
| 63 | + | ||
| 64 | + - uses: pnpm/action-setup@v4 | ||
| 65 | + with: | ||
| 66 | + version: 9 | ||
| 67 | + | ||
| 68 | + - uses: actions/setup-node@v4 | ||
| 69 | + with: | ||
| 70 | + node-version: 20 | ||
| 71 | + cache: pnpm | ||
| 72 | + cache-dependency-path: | | ||
| 73 | + roadTrip/pnpm-lock.yaml | ||
| 74 | + ui-kit/pnpm-lock.yaml | ||
| 75 | + | ||
| 76 | + - name: Build ui-kit | ||
| 77 | + working-directory: ui-kit | ||
| 78 | + run: | | ||
| 79 | + pnpm install --frozen-lockfile | ||
| 80 | + pnpm build | ||
| 81 | + | ||
| 82 | + - name: Install road-trip | ||
| 83 | + working-directory: roadTrip | ||
| 84 | + run: pnpm install --frozen-lockfile | ||
| 85 | + | ||
| 86 | + - name: Test road-trip | ||
| 87 | + working-directory: roadTrip | ||
| 88 | + run: pnpm test --no-watch --karma-config=karma.ci.conf.js | ||
README.md +4 -0 | @@ -25,3 +25,7 @@ Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To u | ||
| 25 | 25 | ## Further help |
| 26 | 26 | |
| 27 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. |
| 28 | + | |
| 29 | +## ui-kit | |
| 30 | + | |
| 31 | +`ui-kit` is linked from `../ui-kit/dist/ui-kit` (temporary, until published). Build it first with `pnpm ui-kit:build` before `pnpm start`, `pnpm build` or `pnpm test`. | |
| @@ -25,3 +25,7 @@ Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To u | |||
| 25 | ## Further help | 25 | ## Further help |
| 26 | 26 | ||
| 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. |
| 28 | + | ||
| 29 | +## ui-kit | ||
| 30 | + | ||
| 31 | +`ui-kit` is linked from `../ui-kit/dist/ui-kit` (temporary, until published). Build it first with `pnpm ui-kit:build` before `pnpm start`, `pnpm build` or `pnpm test`. | ||
angular.json +4 -2 | @@ -35,7 +35,8 @@ | ||
| 35 | 35 | "scripts": [], |
| 36 | 36 | "allowedCommonJsDependencies": [ |
| 37 | 37 | "leaflet" |
| 38 | - ] | |
| 38 | + ], | |
| 39 | + "preserveSymlinks": true | |
| 39 | 40 | }, |
| 40 | 41 | "configurations": { |
| 41 | 42 | "production": { |
| @@ -93,7 +94,8 @@ | ||
| 93 | 94 | "styles": [ |
| 94 | 95 | "src/styles.css" |
| 95 | 96 | ], |
| 96 | - "scripts": [] | |
| 97 | + "scripts": [], | |
| 98 | + "preserveSymlinks": true | |
| 97 | 99 | } |
| 98 | 100 | } |
| 99 | 101 | } |
| @@ -35,7 +35,8 @@ | |||
| 35 | "scripts": [], | 35 | "scripts": [], |
| 36 | "allowedCommonJsDependencies": [ | 36 | "allowedCommonJsDependencies": [ |
| 37 | "leaflet" | 37 | "leaflet" |
| 38 | - ] | 38 | + ], |
| 39 | + "preserveSymlinks": true | ||
| 39 | }, | 40 | }, |
| 40 | "configurations": { | 41 | "configurations": { |
| 41 | "production": { | 42 | "production": { |
| @@ -93,7 +94,8 @@ | |||
| 93 | "styles": [ | 94 | "styles": [ |
| 94 | "src/styles.css" | 95 | "src/styles.css" |
| 95 | ], | 96 | ], |
| 96 | - "scripts": [] | 97 | + "scripts": [], |
| 98 | + "preserveSymlinks": true | ||
| 97 | } | 99 | } |
| 98 | } | 100 | } |
| 99 | } | 101 | } |
karma.ci.conf.js +22 -0 | new file mode 100644 | ||
| @@ -0,0 +1,22 @@ | ||
| 1 | +module.exports = function (config) { | |
| 2 | + config.set({ | |
| 3 | + basePath: '', | |
| 4 | + frameworks: ['jasmine', '@angular-devkit/build-angular'], | |
| 5 | + plugins: [ | |
| 6 | + require('karma-jasmine'), | |
| 7 | + require('karma-chrome-launcher'), | |
| 8 | + require('karma-coverage'), | |
| 9 | + require('@angular-devkit/build-angular/plugins/karma'), | |
| 10 | + ], | |
| 11 | + reporters: ['progress'], | |
| 12 | + browsers: ['ChromeHeadlessCI'], | |
| 13 | + customLaunchers: { | |
| 14 | + ChromeHeadlessCI: { | |
| 15 | + base: 'ChromeHeadless', | |
| 16 | + flags: ['--no-sandbox', '--disable-gpu'], | |
| 17 | + }, | |
| 18 | + }, | |
| 19 | + singleRun: true, | |
| 20 | + restartOnFileChange: false, | |
| 21 | + }); | |
| 22 | +}; | |
| new file mode 100644 | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | +module.exports = function (config) { | ||
| 2 | + config.set({ | ||
| 3 | + basePath: '', | ||
| 4 | + frameworks: ['jasmine', '@angular-devkit/build-angular'], | ||
| 5 | + plugins: [ | ||
| 6 | + require('karma-jasmine'), | ||
| 7 | + require('karma-chrome-launcher'), | ||
| 8 | + require('karma-coverage'), | ||
| 9 | + require('@angular-devkit/build-angular/plugins/karma'), | ||
| 10 | + ], | ||
| 11 | + reporters: ['progress'], | ||
| 12 | + browsers: ['ChromeHeadlessCI'], | ||
| 13 | + customLaunchers: { | ||
| 14 | + ChromeHeadlessCI: { | ||
| 15 | + base: 'ChromeHeadless', | ||
| 16 | + flags: ['--no-sandbox', '--disable-gpu'], | ||
| 17 | + }, | ||
| 18 | + }, | ||
| 19 | + singleRun: true, | ||
| 20 | + restartOnFileChange: false, | ||
| 21 | + }); | ||
| 22 | +}; | ||
package.json +2 -0 | @@ -3,6 +3,7 @@ | ||
| 3 | 3 | "version": "0.0.0", |
| 4 | 4 | "scripts": { |
| 5 | 5 | "ng": "ng", |
| 6 | + "ui-kit:build": "pnpm --dir ../ui-kit build", | |
| 6 | 7 | "start": "ng serve", |
| 7 | 8 | "build": "ng build", |
| 8 | 9 | "watch": "ng build --watch --configuration development", |
| @@ -21,6 +22,7 @@ | ||
| 21 | 22 | "leaflet": "^1.9.4", |
| 22 | 23 | "rxjs": "~7.8.0", |
| 23 | 24 | "tslib": "^2.3.0", |
| 25 | + "ui-kit": "link:../ui-kit/dist/ui-kit", | |
| 24 | 26 | "zone.js": "~0.14.10" |
| 25 | 27 | }, |
| 26 | 28 | "devDependencies": { |
| @@ -3,6 +3,7 @@ | |||
| 3 | "version": "0.0.0", | 3 | "version": "0.0.0", |
| 4 | "scripts": { | 4 | "scripts": { |
| 5 | "ng": "ng", | 5 | "ng": "ng", |
| 6 | + "ui-kit:build": "pnpm --dir ../ui-kit build", | ||
| 6 | "start": "ng serve", | 7 | "start": "ng serve", |
| 7 | "build": "ng build", | 8 | "build": "ng build", |
| 8 | "watch": "ng build --watch --configuration development", | 9 | "watch": "ng build --watch --configuration development", |
| @@ -21,6 +22,7 @@ | |||
| 21 | "leaflet": "^1.9.4", | 22 | "leaflet": "^1.9.4", |
| 22 | "rxjs": "~7.8.0", | 23 | "rxjs": "~7.8.0", |
| 23 | "tslib": "^2.3.0", | 24 | "tslib": "^2.3.0", |
| 25 | + "ui-kit": "link:../ui-kit/dist/ui-kit", | ||
| 24 | "zone.js": "~0.14.10" | 26 | "zone.js": "~0.14.10" |
| 25 | }, | 27 | }, |
| 26 | "devDependencies": { | 28 | "devDependencies": { |
pnpm-lock.yaml +3 -0 | @@ -41,6 +41,9 @@ importers: | ||
| 41 | 41 | tslib: |
| 42 | 42 | specifier: ^2.3.0 |
| 43 | 43 | version: 2.8.1 |
| 44 | + ui-kit: | |
| 45 | + specifier: link:../ui-kit/dist/ui-kit | |
| 46 | + version: link:../ui-kit/dist/ui-kit | |
| 44 | 47 | zone.js: |
| 45 | 48 | specifier: ~0.14.10 |
| 46 | 49 | version: 0.14.10 |
| @@ -41,6 +41,9 @@ importers: | |||
| 41 | tslib: | 41 | tslib: |
| 42 | specifier: ^2.3.0 | 42 | specifier: ^2.3.0 |
| 43 | version: 2.8.1 | 43 | version: 2.8.1 |
| 44 | + ui-kit: | ||
| 45 | + specifier: link:../ui-kit/dist/ui-kit | ||
| 46 | + version: link:../ui-kit/dist/ui-kit | ||
| 44 | zone.js: | 47 | zone.js: |
| 45 | specifier: ~0.14.10 | 48 | specifier: ~0.14.10 |
| 46 | version: 0.14.10 | 49 | version: 0.14.10 |
src/app/app.component.css +1 -1 | @@ -48,7 +48,7 @@ nav a:hover { | ||
| 48 | 48 | |
| 49 | 49 | nav a.is-active { |
| 50 | 50 | background: var(--visited); |
| 51 | - color: var(--on-accent); | |
| 51 | + color: var(--ui-on-accent); | |
| 52 | 52 | } |
| 53 | 53 | |
| 54 | 54 | .count { |
| @@ -48,7 +48,7 @@ nav a:hover { | |||
| 48 | 48 | ||
| 49 | nav a.is-active { | 49 | nav a.is-active { |
| 50 | background: var(--visited); | 50 | background: var(--visited); |
| 51 | - color: var(--on-accent); | 51 | + color: var(--ui-on-accent); |
| 52 | } | 52 | } |
| 53 | 53 | ||
| 54 | .count { | 54 | .count { |
src/app/travel/components/rating/rating.component.ts +0 -29 | deleted file mode 100644 | ||
| @@ -1,29 +0,0 @@ | ||
| 1 | -import { NgIf } from '@angular/common'; | |
| 2 | -import { Component, Input } from '@angular/core'; | |
| 3 | - | |
| 4 | -@Component({ | |
| 5 | - selector: 'app-rating', | |
| 6 | - standalone: true, | |
| 7 | - imports: [NgIf], | |
| 8 | - template: ` | |
| 9 | - <span *ngIf="value !== undefined; else noRating" class="rating" role="img" [attr.aria-label]="value + ' sur 5'">{{ | |
| 10 | - stars | |
| 11 | - }}</span> | |
| 12 | - <ng-template #noRating><span class="muted">Non noté</span></ng-template> | |
| 13 | - `, | |
| 14 | - styles: ` | |
| 15 | - .rating { | |
| 16 | - color: var(--wish); | |
| 17 | - letter-spacing: 0.1em; | |
| 18 | - white-space: nowrap; | |
| 19 | - } | |
| 20 | - `, | |
| 21 | -}) | |
| 22 | -export class RatingComponent { | |
| 23 | - @Input() value?: number; | |
| 24 | - | |
| 25 | - get stars(): string { | |
| 26 | - const filled = Math.max(0, Math.min(5, Math.round(this.value || 0))); | |
| 27 | - return '★'.repeat(filled) + '☆'.repeat(5 - filled); | |
| 28 | - } | |
| 29 | -} | |
| deleted file mode 100644 | |||
| @@ -1,29 +0,0 @@ | |||
| 1 | -import { NgIf } from '@angular/common'; | ||
| 2 | -import { Component, Input } from '@angular/core'; | ||
| 3 | - | ||
| 4 | -@Component({ | ||
| 5 | - selector: 'app-rating', | ||
| 6 | - standalone: true, | ||
| 7 | - imports: [NgIf], | ||
| 8 | - template: ` | ||
| 9 | - <span *ngIf="value !== undefined; else noRating" class="rating" role="img" [attr.aria-label]="value + ' sur 5'">{{ | ||
| 10 | - stars | ||
| 11 | - }}</span> | ||
| 12 | - <ng-template #noRating><span class="muted">Non noté</span></ng-template> | ||
| 13 | - `, | ||
| 14 | - styles: ` | ||
| 15 | - .rating { | ||
| 16 | - color: var(--wish); | ||
| 17 | - letter-spacing: 0.1em; | ||
| 18 | - white-space: nowrap; | ||
| 19 | - } | ||
| 20 | - `, | ||
| 21 | -}) | ||
| 22 | -export class RatingComponent { | ||
| 23 | - @Input() value?: number; | ||
| 24 | - | ||
| 25 | - get stars(): string { | ||
| 26 | - const filled = Math.max(0, Math.min(5, Math.round(this.value || 0))); | ||
| 27 | - return '★'.repeat(filled) + '☆'.repeat(5 - filled); | ||
| 28 | - } | ||
| 29 | -} | ||
src/app/travel/components/travel-map/travel-map.component.spec.ts +5 -5 | @@ -39,9 +39,9 @@ describe('TravelMapComponent', () => { | ||
| 39 | 39 | fixture.componentRef.setInput('places', places); |
| 40 | 40 | const root = render(); |
| 41 | 41 | |
| 42 | - expect(root.querySelectorAll('.pin').length).toBe(2); | |
| 43 | - expect(root.querySelectorAll('.pin--visited').length).toBe(1); | |
| 44 | - expect(root.querySelectorAll('.pin--wishlist').length).toBe(1); | |
| 42 | + expect(root.querySelectorAll('.ui-map-pin').length).toBe(2); | |
| 43 | + expect(root.querySelectorAll('.ui-map-pin--primary').length).toBe(1); | |
| 44 | + expect(root.querySelectorAll('.ui-map-pin--secondary').length).toBe(1); | |
| 45 | 45 | }); |
| 46 | 46 | |
| 47 | 47 | it('met à jour les repères quand la liste change', () => { |
| @@ -51,12 +51,12 @@ describe('TravelMapComponent', () => { | ||
| 51 | 51 | fixture.componentRef.setInput('places', places.slice(0, 1)); |
| 52 | 52 | fixture.detectChanges(); |
| 53 | 53 | |
| 54 | - expect(root.querySelectorAll('.pin').length).toBe(1); | |
| 54 | + expect(root.querySelectorAll('.ui-map-pin').length).toBe(1); | |
| 55 | 55 | }); |
| 56 | 56 | |
| 57 | 57 | it('affiche le point à placer', () => { |
| 58 | 58 | fixture.componentRef.setInput('picked', { lat: 10, lng: 20 }); |
| 59 | - expect(render().querySelectorAll('.pin--picked').length).toBe(1); | |
| 59 | + expect(render().querySelectorAll('.ui-map-pin--highlight').length).toBe(1); | |
| 60 | 60 | }); |
| 61 | 61 | |
| 62 | 62 | it('émet les coordonnées au clic quand la sélection est activée', () => { |
| @@ -39,9 +39,9 @@ describe('TravelMapComponent', () => { | |||
| 39 | fixture.componentRef.setInput('places', places); | 39 | fixture.componentRef.setInput('places', places); |
| 40 | const root = render(); | 40 | const root = render(); |
| 41 | 41 | ||
| 42 | - expect(root.querySelectorAll('.pin').length).toBe(2); | 42 | + expect(root.querySelectorAll('.ui-map-pin').length).toBe(2); |
| 43 | - expect(root.querySelectorAll('.pin--visited').length).toBe(1); | 43 | + expect(root.querySelectorAll('.ui-map-pin--primary').length).toBe(1); |
| 44 | - expect(root.querySelectorAll('.pin--wishlist').length).toBe(1); | 44 | + expect(root.querySelectorAll('.ui-map-pin--secondary').length).toBe(1); |
| 45 | }); | 45 | }); |
| 46 | 46 | ||
| 47 | it('met à jour les repères quand la liste change', () => { | 47 | it('met à jour les repères quand la liste change', () => { |
| @@ -51,12 +51,12 @@ describe('TravelMapComponent', () => { | |||
| 51 | fixture.componentRef.setInput('places', places.slice(0, 1)); | 51 | fixture.componentRef.setInput('places', places.slice(0, 1)); |
| 52 | fixture.detectChanges(); | 52 | fixture.detectChanges(); |
| 53 | 53 | ||
| 54 | - expect(root.querySelectorAll('.pin').length).toBe(1); | 54 | + expect(root.querySelectorAll('.ui-map-pin').length).toBe(1); |
| 55 | }); | 55 | }); |
| 56 | 56 | ||
| 57 | it('affiche le point à placer', () => { | 57 | it('affiche le point à placer', () => { |
| 58 | fixture.componentRef.setInput('picked', { lat: 10, lng: 20 }); | 58 | fixture.componentRef.setInput('picked', { lat: 10, lng: 20 }); |
| 59 | - expect(render().querySelectorAll('.pin--picked').length).toBe(1); | 59 | + expect(render().querySelectorAll('.ui-map-pin--highlight').length).toBe(1); |
| 60 | }); | 60 | }); |
| 61 | 61 | ||
| 62 | it('émet les coordonnées au clic quand la sélection est activée', () => { | 62 | it('émet les coordonnées au clic quand la sélection est activée', () => { |
src/app/travel/components/travel-map/travel-map.component.ts +28 -143 | @@ -1,60 +1,30 @@ | ||
| 1 | -import { | |
| 2 | - AfterViewInit, | |
| 3 | - Component, | |
| 4 | - ElementRef, | |
| 5 | - EventEmitter, | |
| 6 | - Input, | |
| 7 | - OnChanges, | |
| 8 | - OnDestroy, | |
| 9 | - Output, | |
| 10 | - SimpleChanges, | |
| 11 | - ViewChild, | |
| 12 | - ViewEncapsulation, | |
| 13 | -} from '@angular/core'; | |
| 14 | -import * as L from 'leaflet'; | |
| 1 | +import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; | |
| 2 | +import { UiMapComponent, UiMapPoint } from 'ui-kit'; | |
| 15 | 3 | import { CATEGORY_LABELS, LatLng, Place } from '../../models/place.model'; |
| 16 | 4 | import { formatIsoDate } from '../../utils/date'; |
| 17 | 5 | |
| 18 | -type PinKind = Place['status'] | 'picked'; | |
| 19 | - | |
| 6 | +/** Adaptateur entre le domaine RoadTrip (lieux) et la carte générique de la bibliothèque ui-kit. */ | |
| 20 | 7 | @Component({ |
| 21 | 8 | selector: 'app-travel-map', |
| 22 | 9 | standalone: true, |
| 23 | - // Les repères sont créés par Leaflet hors du template : leurs styles doivent être globaux. | |
| 24 | - encapsulation: ViewEncapsulation.None, | |
| 25 | - template: `<div #surface class="travel-map" role="region" [attr.aria-label]="label"></div>`, | |
| 10 | + imports: [UiMapComponent], | |
| 11 | + template: ` | |
| 12 | + <ui-map | |
| 13 | + [points]="points" | |
| 14 | + [selectable]="pickable" | |
The previous drawPlaces() auto-fitted bounds to the markers when not pickable (with padding and maxZoom 5). Confirm UiMapComponent preserves this viewport behavior, or the initial map view will change for users with existing places. | ||
| 15 | + [selected]="picked" | |
| 16 | + [label]="label" | |
| 17 | + (pointSelected)="locationPicked.emit($event)" | |
| 18 | + ></ui-map> | |
pointSelected's $event is forwarded straight to locationPicked: EventEmitter. Confirm UiMapComponent emits a plain {lat, lng} and not the full UiMapPoint — the old code also rounded coordinates to 4 decimals; that normalization is now gone. The spec only asserts shape ({lat, lng} numbers), so a mismatch here would slip through tests. The old implementation rounded emitted pick coordinates to 4 decimals (round()); that behavior now depends on UiMapComponent's pointSelected event. If ui-kit emits raw lat/lng, picked values will carry full float precision — worth confirming this is intended. | ||
| 19 | + `, | |
| 26 | 20 | styles: ` |
| 27 | - app-travel-map { | |
| 21 | + :host { | |
| 28 | 22 | display: block; |
| 29 | 23 | height: 100%; |
| 30 | 24 | } |
| 31 | - .travel-map { | |
| 32 | - position: relative; | |
| 33 | - z-index: 0; | |
| 34 | - height: 100%; | |
| 35 | - min-height: 320px; | |
| 36 | - border-radius: var(--radius); | |
| 37 | - } | |
| 38 | - .pin { | |
| 39 | - box-sizing: border-box; | |
| 40 | - border: 3px solid #fff; | |
| 41 | - border-radius: 50%; | |
| 42 | - box-shadow: 0 1px 5px rgb(0 0 0 / 0.5); | |
| 43 | - } | |
| 44 | - .pin--visited { | |
| 45 | - background: var(--visited); | |
| 46 | - } | |
| 47 | - .pin--wishlist { | |
| 48 | - background: #fff; | |
| 49 | - border: 3px dashed var(--wish); | |
| 50 | - } | |
| 51 | - .pin--picked { | |
| 52 | - background: #111; | |
| 53 | - box-shadow: 0 0 0 4px rgb(255 255 255 / 0.85), 0 1px 6px rgb(0 0 0 / 0.6); | |
| 54 | - } | |
| 55 | 25 | `, |
| 56 | 26 | }) |
| 57 | -export class TravelMapComponent implements OnChanges, AfterViewInit, OnDestroy { | |
| 27 | +export class TravelMapComponent implements OnChanges { | |
| 58 | 28 | @Input() places: readonly Place[] = []; |
| 59 | 29 | /** Active la sélection d'un point par clic sur la carte. */ |
| 60 | 30 | @Input() pickable = false; |
| @@ -64,108 +34,23 @@ export class TravelMapComponent implements OnChanges, AfterViewInit, OnDestroy { | ||
| 64 | 34 | |
| 65 | 35 | @Output() locationPicked = new EventEmitter<LatLng>(); |
| 66 | 36 | |
| 67 | - @ViewChild('surface', { static: true }) surface!: ElementRef<HTMLDivElement>; | |
| 68 | - | |
| 69 | - private readonly markers = L.layerGroup(); | |
| 70 | - private map: L.Map | undefined; | |
| 71 | - private pickedMarker: L.Marker | undefined; | |
| 37 | + points: UiMapPoint[] = []; | |
| 72 | 38 | |
| 73 | - ngOnChanges(changes: SimpleChanges): void { | |
| 74 | - // Avant ngAfterViewInit, la carte n'existe pas encore : le premier dessin y est fait. | |
| 75 | - if (!this.map) { | |
| 76 | - return; | |
| 77 | - } | |
| 78 | - if (changes['places']) { | |
| 79 | - this.drawPlaces(this.places); | |
| 80 | - } | |
| 81 | - if (changes['picked']) { | |
| 82 | - this.drawPicked(this.picked); | |
| 83 | - } | |
| 39 | + ngOnChanges(): void { | |
| 40 | + this.points = this.places.map(toPoint); | |
| 84 | 41 | } |
| 85 | - | |
| 86 | - ngAfterViewInit(): void { | |
| 87 | - const map = L.map(this.surface.nativeElement, { worldCopyJump: true, minZoom: 2 }).setView([30, 10], 2); | |
| 88 | - | |
| 89 | - L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { | |
| 90 | - maxZoom: 18, | |
| 91 | - attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>', | |
| 92 | - }).addTo(map); | |
| 93 | - this.markers.addTo(map); | |
| 94 | - | |
| 95 | - map.on('click', (event: L.LeafletMouseEvent) => { | |
| 96 | - if (this.pickable) { | |
| 97 | - const { lat, lng } = event.latlng.wrap(); | |
| 98 | - this.locationPicked.emit({ lat: round(lat), lng: round(lng) }); | |
| 99 | - } | |
| 100 | - }); | |
| 101 | - | |
| 102 | - this.map = map; | |
| 103 | - this.drawPlaces(this.places); | |
| 104 | - this.drawPicked(this.picked); | |
| 105 | - } | |
| 106 | - | |
| 107 | - ngOnDestroy(): void { | |
| 108 | - this.map?.remove(); | |
| 109 | - this.map = undefined; | |
| 110 | - } | |
| 111 | - | |
| 112 | - private drawPlaces(places: readonly Place[]): void { | |
| 113 | - this.markers.clearLayers(); | |
| 114 | - | |
| 115 | - for (const place of places) { | |
| 116 | - L.marker([place.lat, place.lng], { icon: pinIcon(place.status), title: place.name }) | |
| 117 | - .bindPopup(popupContent(place)) | |
| 118 | - .addTo(this.markers); | |
| 119 | - } | |
| 120 | - | |
| 121 | - if (!this.pickable && places.length > 0) { | |
| 122 | - const bounds = L.latLngBounds(places.map((place): L.LatLngTuple => [place.lat, place.lng])); | |
| 123 | - // Marge plus large en haut à gauche pour ne pas passer sous les boutons de zoom. | |
| 124 | - this.map?.fitBounds(bounds, { paddingTopLeft: [64, 64], paddingBottomRight: [40, 40], maxZoom: 5 }); | |
| 125 | - } | |
| 126 | - } | |
| 127 | - | |
| 128 | - private drawPicked(point: LatLng | null): void { | |
| 129 | - this.pickedMarker?.remove(); | |
| 130 | - this.pickedMarker = undefined; | |
| 131 | - | |
| 132 | - if (point && this.map) { | |
| 133 | - this.pickedMarker = L.marker([point.lat, point.lng], { | |
| 134 | - icon: pinIcon('picked'), | |
| 135 | - interactive: false, | |
| 136 | - keyboard: false, | |
| 137 | - }).addTo(this.map); | |
| 138 | - this.map.panTo([point.lat, point.lng]); | |
| 139 | - } | |
| 140 | - } | |
| 141 | -} | |
| 142 | - | |
| 143 | -function pinIcon(kind: PinKind): L.DivIcon { | |
| 144 | - return L.divIcon({ | |
| 145 | - className: `pin pin--${kind}`, | |
| 146 | - iconSize: [18, 18], | |
| 147 | - iconAnchor: [9, 9], | |
| 148 | - popupAnchor: [0, -10], | |
| 149 | - }); | |
| 150 | 42 | } |
| 151 | 43 | |
| 152 | -/** Contenu de la bulle construit via le DOM : les saisies de l'utilisateur ne sont jamais interprétées comme du HTML. */ | |
| 153 | -function popupContent(place: Place): HTMLElement { | |
| 154 | - const root = document.createElement('div'); | |
| 155 | - const title = document.createElement('strong'); | |
| 156 | - title.textContent = place.name; | |
| 157 | - | |
| 158 | - const meta = document.createElement('div'); | |
| 159 | - meta.textContent = `${place.country} · ${CATEGORY_LABELS[place.category]}`; | |
| 160 | - | |
| 161 | - const status = document.createElement('div'); | |
| 162 | - status.textContent = | |
| 44 | +function toPoint(place: Place): UiMapPoint { | |
| 45 | + const status = | |
| 163 | 46 | place.status === 'visited' && place.visitedOn ? `Visité le ${formatIsoDate(place.visitedOn)}` : 'À visiter'; |
| 164 | 47 | |
| 165 | - root.append(title, meta, status); | |
| 166 | - return root; | |
| 167 | -} | |
| 168 | - | |
| 169 | -function round(value: number): number { | |
| 170 | - return Math.round(value * 1e4) / 1e4; | |
| 48 | + return { | |
| 49 | + id: place.id, | |
The old implementation auto-fitted bounds to the places and panned to the picked point; the new adapter only forwards points/selected. If UiMapComponent doesn't implement equivalent fit/pan behavior, this is a user-visible regression (map stays at default world view after adding places). Please confirm the library handles it. | ||
| 50 | + lat: place.lat, | |
| 51 | + lng: place.lng, | |
| 52 | + kind: place.status === 'visited' ? 'primary' : 'secondary', | |
| 53 | + title: place.name, | |
| 54 | + details: [`${place.country} · ${CATEGORY_LABELS[place.category]}`, status], | |
| 55 | + }; | |
| 171 | 56 | } |
| @@ -1,60 +1,30 @@ | |||
| 1 | -import { | 1 | +import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; |
| 2 | - AfterViewInit, | 2 | +import { UiMapComponent, UiMapPoint } from 'ui-kit'; |
| 3 | - Component, | ||
| 4 | - ElementRef, | ||
| 5 | - EventEmitter, | ||
| 6 | - Input, | ||
| 7 | - OnChanges, | ||
| 8 | - OnDestroy, | ||
| 9 | - Output, | ||
| 10 | - SimpleChanges, | ||
| 11 | - ViewChild, | ||
| 12 | - ViewEncapsulation, | ||
| 13 | -} from '@angular/core'; | ||
| 14 | -import * as L from 'leaflet'; | ||
| 15 | import { CATEGORY_LABELS, LatLng, Place } from '../../models/place.model'; | 3 | import { CATEGORY_LABELS, LatLng, Place } from '../../models/place.model'; |
| 16 | import { formatIsoDate } from '../../utils/date'; | 4 | import { formatIsoDate } from '../../utils/date'; |
| 17 | 5 | ||
| 18 | -type PinKind = Place['status'] | 'picked'; | 6 | +/** Adaptateur entre le domaine RoadTrip (lieux) et la carte générique de la bibliothèque ui-kit. */ |
| 19 | - | ||
| 20 | @Component({ | 7 | @Component({ |
| 21 | selector: 'app-travel-map', | 8 | selector: 'app-travel-map', |
| 22 | standalone: true, | 9 | standalone: true, |
| 23 | - // Les repères sont créés par Leaflet hors du template : leurs styles doivent être globaux. | 10 | + imports: [UiMapComponent], |
| 24 | - encapsulation: ViewEncapsulation.None, | 11 | + template: ` |
| 25 | - template: `<div #surface class="travel-map" role="region" [attr.aria-label]="label"></div>`, | 12 | + <ui-map |
| 13 | + [points]="points" | ||
| 14 | + [selectable]="pickable" | ||
| 15 | + [selected]="picked" | ||
| 16 | + [label]="label" | ||
| 17 | + (pointSelected)="locationPicked.emit($event)" | ||
| 18 | + ></ui-map> | ||
| 19 | + `, | ||
| 26 | styles: ` | 20 | styles: ` |
| 27 | - app-travel-map { | 21 | + :host { |
| 28 | display: block; | 22 | display: block; |
| 29 | height: 100%; | 23 | height: 100%; |
| 30 | } | 24 | } |
| 31 | - .travel-map { | ||
| 32 | - position: relative; | ||
| 33 | - z-index: 0; | ||
| 34 | - height: 100%; | ||
| 35 | - min-height: 320px; | ||
| 36 | - border-radius: var(--radius); | ||
| 37 | - } | ||
| 38 | - .pin { | ||
| 39 | - box-sizing: border-box; | ||
| 40 | - border: 3px solid #fff; | ||
| 41 | - border-radius: 50%; | ||
| 42 | - box-shadow: 0 1px 5px rgb(0 0 0 / 0.5); | ||
| 43 | - } | ||
| 44 | - .pin--visited { | ||
| 45 | - background: var(--visited); | ||
| 46 | - } | ||
| 47 | - .pin--wishlist { | ||
| 48 | - background: #fff; | ||
| 49 | - border: 3px dashed var(--wish); | ||
| 50 | - } | ||
| 51 | - .pin--picked { | ||
| 52 | - background: #111; | ||
| 53 | - box-shadow: 0 0 0 4px rgb(255 255 255 / 0.85), 0 1px 6px rgb(0 0 0 / 0.6); | ||
| 54 | - } | ||
| 55 | `, | 25 | `, |
| 56 | }) | 26 | }) |
| 57 | -export class TravelMapComponent implements OnChanges, AfterViewInit, OnDestroy { | 27 | +export class TravelMapComponent implements OnChanges { |
| 58 | @Input() places: readonly Place[] = []; | 28 | @Input() places: readonly Place[] = []; |
| 59 | /** Active la sélection d'un point par clic sur la carte. */ | 29 | /** Active la sélection d'un point par clic sur la carte. */ |
| 60 | @Input() pickable = false; | 30 | @Input() pickable = false; |
| @@ -64,108 +34,23 @@ export class TravelMapComponent implements OnChanges, AfterViewInit, OnDestroy { | |||
| 64 | 34 | ||
| 65 | @Output() locationPicked = new EventEmitter<LatLng>(); | 35 | @Output() locationPicked = new EventEmitter<LatLng>(); |
| 66 | 36 | ||
| 67 | - @ViewChild('surface', { static: true }) surface!: ElementRef<HTMLDivElement>; | 37 | + points: UiMapPoint[] = []; |
| 68 | - | ||
| 69 | - private readonly markers = L.layerGroup(); | ||
| 70 | - private map: L.Map | undefined; | ||
| 71 | - private pickedMarker: L.Marker | undefined; | ||
| 72 | 38 | ||
| 73 | - ngOnChanges(changes: SimpleChanges): void { | 39 | + ngOnChanges(): void { |
| 74 | - // Avant ngAfterViewInit, la carte n'existe pas encore : le premier dessin y est fait. | 40 | + this.points = this.places.map(toPoint); |
| 75 | - if (!this.map) { | ||
| 76 | - return; | ||
| 77 | - } | ||
| 78 | - if (changes['places']) { | ||
| 79 | - this.drawPlaces(this.places); | ||
| 80 | - } | ||
| 81 | - if (changes['picked']) { | ||
| 82 | - this.drawPicked(this.picked); | ||
| 83 | - } | ||
| 84 | } | 41 | } |
| 85 | - | ||
| 86 | - ngAfterViewInit(): void { | ||
| 87 | - const map = L.map(this.surface.nativeElement, { worldCopyJump: true, minZoom: 2 }).setView([30, 10], 2); | ||
| 88 | - | ||
| 89 | - L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { | ||
| 90 | - maxZoom: 18, | ||
| 91 | - attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>', | ||
| 92 | - }).addTo(map); | ||
| 93 | - this.markers.addTo(map); | ||
| 94 | - | ||
| 95 | - map.on('click', (event: L.LeafletMouseEvent) => { | ||
| 96 | - if (this.pickable) { | ||
| 97 | - const { lat, lng } = event.latlng.wrap(); | ||
| 98 | - this.locationPicked.emit({ lat: round(lat), lng: round(lng) }); | ||
| 99 | - } | ||
| 100 | - }); | ||
| 101 | - | ||
| 102 | - this.map = map; | ||
| 103 | - this.drawPlaces(this.places); | ||
| 104 | - this.drawPicked(this.picked); | ||
| 105 | - } | ||
| 106 | - | ||
| 107 | - ngOnDestroy(): void { | ||
| 108 | - this.map?.remove(); | ||
| 109 | - this.map = undefined; | ||
| 110 | - } | ||
| 111 | - | ||
| 112 | - private drawPlaces(places: readonly Place[]): void { | ||
| 113 | - this.markers.clearLayers(); | ||
| 114 | - | ||
| 115 | - for (const place of places) { | ||
| 116 | - L.marker([place.lat, place.lng], { icon: pinIcon(place.status), title: place.name }) | ||
| 117 | - .bindPopup(popupContent(place)) | ||
| 118 | - .addTo(this.markers); | ||
| 119 | - } | ||
| 120 | - | ||
| 121 | - if (!this.pickable && places.length > 0) { | ||
| 122 | - const bounds = L.latLngBounds(places.map((place): L.LatLngTuple => [place.lat, place.lng])); | ||
| 123 | - // Marge plus large en haut à gauche pour ne pas passer sous les boutons de zoom. | ||
| 124 | - this.map?.fitBounds(bounds, { paddingTopLeft: [64, 64], paddingBottomRight: [40, 40], maxZoom: 5 }); | ||
| 125 | - } | ||
| 126 | - } | ||
| 127 | - | ||
| 128 | - private drawPicked(point: LatLng | null): void { | ||
| 129 | - this.pickedMarker?.remove(); | ||
| 130 | - this.pickedMarker = undefined; | ||
| 131 | - | ||
| 132 | - if (point && this.map) { | ||
| 133 | - this.pickedMarker = L.marker([point.lat, point.lng], { | ||
| 134 | - icon: pinIcon('picked'), | ||
| 135 | - interactive: false, | ||
| 136 | - keyboard: false, | ||
| 137 | - }).addTo(this.map); | ||
| 138 | - this.map.panTo([point.lat, point.lng]); | ||
| 139 | - } | ||
| 140 | - } | ||
| 141 | -} | ||
| 142 | - | ||
| 143 | -function pinIcon(kind: PinKind): L.DivIcon { | ||
| 144 | - return L.divIcon({ | ||
| 145 | - className: `pin pin--${kind}`, | ||
| 146 | - iconSize: [18, 18], | ||
| 147 | - iconAnchor: [9, 9], | ||
| 148 | - popupAnchor: [0, -10], | ||
| 149 | - }); | ||
| 150 | } | 42 | } |
| 151 | 43 | ||
| 152 | -/** Contenu de la bulle construit via le DOM : les saisies de l'utilisateur ne sont jamais interprétées comme du HTML. */ | 44 | +function toPoint(place: Place): UiMapPoint { |
| 153 | -function popupContent(place: Place): HTMLElement { | 45 | + const status = |
| 154 | - const root = document.createElement('div'); | ||
| 155 | - const title = document.createElement('strong'); | ||
| 156 | - title.textContent = place.name; | ||
| 157 | - | ||
| 158 | - const meta = document.createElement('div'); | ||
| 159 | - meta.textContent = `${place.country} · ${CATEGORY_LABELS[place.category]}`; | ||
| 160 | - | ||
| 161 | - const status = document.createElement('div'); | ||
| 162 | - status.textContent = | ||
| 163 | place.status === 'visited' && place.visitedOn ? `Visité le ${formatIsoDate(place.visitedOn)}` : 'À visiter'; | 46 | place.status === 'visited' && place.visitedOn ? `Visité le ${formatIsoDate(place.visitedOn)}` : 'À visiter'; |
| 164 | 47 | ||
| 165 | - root.append(title, meta, status); | 48 | + return { |
| 166 | - return root; | 49 | + id: place.id, |
| 167 | -} | 50 | + lat: place.lat, |
| 168 | - | 51 | + lng: place.lng, |
| 169 | -function round(value: number): number { | 52 | + kind: place.status === 'visited' ? 'primary' : 'secondary', |
| 170 | - return Math.round(value * 1e4) / 1e4; | 53 | + title: place.name, |
| 54 | + details: [`${place.country} · ${CATEGORY_LABELS[place.category]}`, status], | ||
| 55 | + }; | ||
| 171 | } | 56 | } |
src/app/travel/pages/place-form-page/place-form-page.component.html +87 -113 | @@ -1,134 +1,108 @@ | ||
| 1 | 1 | <section class="page"> |
| 2 | - <header class="page__header"> | |
| 3 | - <p class="eyebrow">Nouveau lieu</p> | |
| 4 | - <h1>Ajouter un lieu à votre carnet</h1> | |
| 5 | - </header> | |
| 2 | + <ui-page-header eyebrow="Nouveau lieu" heading="Ajouter un lieu à votre carnet"></ui-page-header> | |
| 6 | 3 | |
| 7 | 4 | <div class="layout"> |
| 8 | - <form class="card form" [formGroup]="form" (ngSubmit)="submit()" novalidate> | |
| 9 | - <fieldset class="segmented"> | |
| 10 | - <legend>Ce lieu est…</legend> | |
| 11 | - <label> | |
| 12 | - <input type="radio" formControlName="status" value="visited" /> | |
| 13 | - <span>Déjà visité</span> | |
| 14 | - </label> | |
| 15 | - <label> | |
| 16 | - <input type="radio" formControlName="status" value="wishlist" /> | |
| 17 | - <span>Sur ma liste de souhaits</span> | |
| 18 | - </label> | |
| 19 | - </fieldset> | |
| 5 | + <ui-card> | |
| 6 | + <form class="form" [formGroup]="form" (ngSubmit)="submit()" novalidate> | |
| 7 | + <ui-segmented legend="Ce lieu est…" formControlName="status" [options]="statusOptions"></ui-segmented> | |
| 20 | 8 | |
| 21 | - <div class="field"> | |
| 22 | - <label for="name">Nom du lieu</label> | |
| 23 | - <input | |
| 24 | - id="name" | |
| 25 | - type="text" | |
| 26 | - formControlName="name" | |
| 27 | - autocomplete="off" | |
| 28 | - placeholder="Annecy, Col de l’Iseran…" | |
| 29 | - [attr.aria-describedby]="errorOf('name') ? 'name-error' : null" | |
| 30 | - /> | |
| 31 | - <p *ngIf="errorOf('name') as message" class="field__error" id="name-error" role="alert">{{ message }}</p> | |
| 32 | - </div> | |
| 33 | - | |
| 34 | - <div class="row"> | |
| 35 | - <div class="field"> | |
| 36 | - <label for="country">Pays</label> | |
| 9 | + <ui-field label="Nom du lieu" controlId="name" [error]="errorOf('name')"> | |
| 37 | 10 | <input |
| 38 | - id="country" | |
| 11 | + id="name" | |
| 39 | 12 | type="text" |
| 40 | - formControlName="country" | |
| 41 | - autocomplete="country-name" | |
| 42 | - [attr.aria-describedby]="errorOf('country') ? 'country-error' : null" | |
| 13 | + formControlName="name" | |
| 14 | + autocomplete="off" | |
| 15 | + placeholder="Annecy, Col de l’Iseran…" | |
| 16 | + [attr.aria-describedby]="errorOf('name') ? 'name-error' : null" | |
Les inputs conservent [attr.aria-describedby]="errorOf('name') ? 'name-error' : null" alors que le rendu de l'erreur a été délégué à ui-field via [error]. Si ui-field relie déjà l'input au message (via controlId) et génère son propre id, ce binding est redondant ; si l'id généré diffère de '-error', le lien aria devient cassé. Vérifier que les ids produits par ui-field correspondent bien à ce qui est référencé ici. | ||
| 43 | 17 | /> |
| 44 | - <p *ngIf="errorOf('country') as message" class="field__error" id="country-error" role="alert">{{ message }}</p> | |
| 45 | - </div> | |
| 18 | + </ui-field> | |
| 46 | 19 | |
| 47 | - <div class="field"> | |
| 48 | - <label for="category">Type de lieu</label> | |
| 49 | - <select id="category" formControlName="category"> | |
| 50 | - <option *ngFor="let category of categories" [ngValue]="category">{{ categoryLabels[category] }}</option> | |
| 51 | - </select> | |
| 52 | - </div> | |
| 53 | - </div> | |
| 20 | + <div class="row"> | |
| 21 | + <ui-field label="Pays" controlId="country" [error]="errorOf('country')"> | |
| 22 | + <input | |
| 23 | + id="country" | |
| 24 | + type="text" | |
| 25 | + formControlName="country" | |
| 26 | + autocomplete="country-name" | |
| 27 | + [attr.aria-describedby]="errorOf('country') ? 'country-error' : null" | |
| 28 | + /> | |
| 29 | + </ui-field> | |
| 54 | 30 | |
| 55 | - <div class="row"> | |
| 56 | - <div class="field"> | |
| 57 | - <label for="lat">Latitude</label> | |
| 58 | - <input | |
| 59 | - id="lat" | |
| 60 | - type="number" | |
| 61 | - step="any" | |
| 62 | - inputmode="decimal" | |
| 63 | - formControlName="lat" | |
| 64 | - [attr.aria-describedby]="errorOf('lat') ? 'lat-error' : null" | |
| 65 | - /> | |
| 66 | - <p *ngIf="errorOf('lat') as message" class="field__error" id="lat-error" role="alert">{{ message }}</p> | |
| 31 | + <ui-field label="Type de lieu" controlId="category"> | |
| 32 | + <select id="category" formControlName="category"> | |
| 33 | + <option *ngFor="let category of categories" [ngValue]="category">{{ categoryLabels[category] }}</option> | |
| 34 | + </select> | |
| 35 | + </ui-field> | |
| 67 | 36 | </div> |
| 68 | 37 | |
| 69 | - <div class="field"> | |
| 70 | - <label for="lng">Longitude</label> | |
| 71 | - <input | |
| 72 | - id="lng" | |
| 73 | - type="number" | |
| 74 | - step="any" | |
| 75 | - inputmode="decimal" | |
| 76 | - formControlName="lng" | |
| 77 | - [attr.aria-describedby]="errorOf('lng') ? 'lng-error' : null" | |
| 78 | - /> | |
| 79 | - <p *ngIf="errorOf('lng') as message" class="field__error" id="lng-error" role="alert">{{ message }}</p> | |
| 80 | - </div> | |
| 81 | - </div> | |
| 82 | - <p class="hint">Astuce : cliquez sur la carte pour remplir la latitude et la longitude.</p> | |
| 38 | + <div class="row"> | |
| 39 | + <ui-field label="Latitude" controlId="lat" [error]="errorOf('lat')"> | |
| 40 | + <input | |
| 41 | + id="lat" | |
| 42 | + type="number" | |
| 43 | + step="any" | |
| 44 | + inputmode="decimal" | |
| 45 | + formControlName="lat" | |
| 46 | + [attr.aria-describedby]="errorOf('lat') ? 'lat-error' : null" | |
| 47 | + /> | |
| 48 | + </ui-field> | |
| 83 | 49 | |
| 84 | - <ng-container [ngSwitch]="status"> | |
| 85 | - <div class="row" *ngSwitchCase="'visited'"> | |
| 86 | - <div class="field"> | |
| 87 | - <label for="visitedOn">Date de visite</label> | |
| 50 | + <ui-field label="Longitude" controlId="lng" [error]="errorOf('lng')"> | |
| 88 | 51 | <input |
| 89 | - id="visitedOn" | |
| 90 | - type="date" | |
| 91 | - formControlName="visitedOn" | |
| 92 | - [attr.max]="maxDate" | |
| 93 | - [attr.aria-describedby]="errorOf('visitedOn') ? 'visitedOn-error' : null" | |
| 52 | + id="lng" | |
| 53 | + type="number" | |
| 54 | + step="any" | |
| 55 | + inputmode="decimal" | |
| 56 | + formControlName="lng" | |
| 57 | + [attr.aria-describedby]="errorOf('lng') ? 'lng-error' : null" | |
| 94 | 58 | /> |
| 95 | - <p *ngIf="errorOf('visitedOn') as message" class="field__error" id="visitedOn-error" role="alert">{{ message }}</p> | |
| 96 | - </div> | |
| 59 | + </ui-field> | |
| 60 | + </div> | |
| 61 | + <p class="hint">Astuce : cliquez sur la carte pour remplir la latitude et la longitude.</p> | |
| 97 | 62 | |
| 98 | - <div class="field"> | |
| 99 | - <label for="rating">Note (facultative)</label> | |
| 100 | - <select id="rating" formControlName="rating"> | |
| 101 | - <option [ngValue]="null">Pas de note</option> | |
| 102 | - <option *ngFor="let rating of ratings" [ngValue]="rating">{{ rating }} sur 5</option> | |
| 103 | - </select> | |
| 63 | + <ng-container [ngSwitch]="status"> | |
| 64 | + <div class="row" *ngSwitchCase="'visited'"> | |
| 65 | + <ui-field label="Date de visite" controlId="visitedOn" [error]="errorOf('visitedOn')"> | |
| 66 | + <input | |
| 67 | + id="visitedOn" | |
| 68 | + type="date" | |
| 69 | + formControlName="visitedOn" | |
| 70 | + [attr.max]="maxDate" | |
| 71 | + [attr.aria-describedby]="errorOf('visitedOn') ? 'visitedOn-error' : null" | |
| 72 | + /> | |
| 73 | + </ui-field> | |
| 74 | + | |
| 75 | + <ui-field label="Note (facultative)" controlId="rating"> | |
| 76 | + <select id="rating" formControlName="rating"> | |
| 77 | + <option [ngValue]="null">Pas de note</option> | |
| 78 | + <option *ngFor="let rating of ratings" [ngValue]="rating">{{ rating }} sur 5</option> | |
| 79 | + </select> | |
| 80 | + </ui-field> | |
| 104 | 81 | </div> |
| 105 | - </div> | |
| 106 | - <div class="field" *ngSwitchDefault> | |
| 107 | - <label for="priority">Priorité</label> | |
| 108 | - <select id="priority" formControlName="priority"> | |
| 109 | - <option *ngFor="let priority of priorities" [ngValue]="priority">{{ priorityLabels[priority] }}</option> | |
| 110 | - </select> | |
| 111 | - </div> | |
| 112 | - </ng-container> | |
| 82 | + <ui-field *ngSwitchDefault label="Priorité" controlId="priority"> | |
| 83 | + <select id="priority" formControlName="priority"> | |
| 84 | + <option *ngFor="let priority of priorities" [ngValue]="priority">{{ priorityLabels[priority] }}</option> | |
| 85 | + </select> | |
| 86 | + </ui-field> | |
| 87 | + </ng-container> | |
| 113 | 88 | |
| 114 | - <div class="field"> | |
| 115 | - <label for="note">Note personnelle (facultative)</label> | |
| 116 | - <textarea | |
| 117 | - id="note" | |
| 118 | - rows="3" | |
| 119 | - formControlName="note" | |
| 120 | - [attr.aria-describedby]="errorOf('note') ? 'note-error' : null" | |
| 121 | - ></textarea> | |
| 122 | - <p *ngIf="errorOf('note') as message" class="field__error" id="note-error" role="alert">{{ message }}</p> | |
| 123 | - </div> | |
| 89 | + <ui-field label="Note personnelle (facultative)" controlId="note" [error]="errorOf('note')"> | |
| 90 | + <textarea | |
| 91 | + id="note" | |
| 92 | + rows="3" | |
| 93 | + formControlName="note" | |
| 94 | + [attr.aria-describedby]="errorOf('note') ? 'note-error' : null" | |
| 95 | + ></textarea> | |
| 96 | + </ui-field> | |
| 124 | 97 | |
| 125 | - <div class="actions"> | |
| 126 | - <button type="submit" class="btn btn--primary">Enregistrer</button> | |
| 127 | - <a class="btn" routerLink="/resume">Annuler</a> | |
| 128 | - </div> | |
| 129 | - </form> | |
| 98 | + <div class="actions"> | |
| 99 | + <button type="submit" ui-button variant="primary">Enregistrer</button> | |
| 100 | + <a ui-button routerLink="/resume">Annuler</a> | |
| 101 | + </div> | |
| 102 | + </form> | |
| 103 | + </ui-card> | |
| 130 | 104 | |
| 131 | - <section class="card sticky" aria-labelledby="picker-title"> | |
| 105 | + <ui-card class="sticky" aria-labelledby="picker-title"> | |
| 132 | 106 | <h2 id="picker-title">Placer le lieu sur la carte</h2> |
| 133 | 107 | <div class="map-slot"> |
| 134 | 108 | <app-travel-map |
| @@ -139,6 +113,6 @@ | ||
| 139 | 113 | (locationPicked)="onPick($event)" |
| 140 | 114 | ></app-travel-map> |
| 141 | 115 | </div> |
| 142 | - </section> | |
| 116 | + </ui-card> | |
| 143 | 117 | </div> |
| 144 | 118 | </section> |
| @@ -1,134 +1,108 @@ | |||
| 1 | <section class="page"> | 1 | <section class="page"> |
| 2 | - <header class="page__header"> | 2 | + <ui-page-header eyebrow="Nouveau lieu" heading="Ajouter un lieu à votre carnet"></ui-page-header> |
| 3 | - <p class="eyebrow">Nouveau lieu</p> | ||
| 4 | - <h1>Ajouter un lieu à votre carnet</h1> | ||
| 5 | - </header> | ||
| 6 | 3 | ||
| 7 | <div class="layout"> | 4 | <div class="layout"> |
| 8 | - <form class="card form" [formGroup]="form" (ngSubmit)="submit()" novalidate> | 5 | + <ui-card> |
| 9 | - <fieldset class="segmented"> | 6 | + <form class="form" [formGroup]="form" (ngSubmit)="submit()" novalidate> |
| 10 | - <legend>Ce lieu est…</legend> | 7 | + <ui-segmented legend="Ce lieu est…" formControlName="status" [options]="statusOptions"></ui-segmented> |
| 11 | - <label> | ||
| 12 | - <input type="radio" formControlName="status" value="visited" /> | ||
| 13 | - <span>Déjà visité</span> | ||
| 14 | - </label> | ||
| 15 | - <label> | ||
| 16 | - <input type="radio" formControlName="status" value="wishlist" /> | ||
| 17 | - <span>Sur ma liste de souhaits</span> | ||
| 18 | - </label> | ||
| 19 | - </fieldset> | ||
| 20 | 8 | ||
| 21 | - <div class="field"> | 9 | + <ui-field label="Nom du lieu" controlId="name" [error]="errorOf('name')"> |
| 22 | - <label for="name">Nom du lieu</label> | ||
| 23 | - <input | ||
| 24 | - id="name" | ||
| 25 | - type="text" | ||
| 26 | - formControlName="name" | ||
| 27 | - autocomplete="off" | ||
| 28 | - placeholder="Annecy, Col de l’Iseran…" | ||
| 29 | - [attr.aria-describedby]="errorOf('name') ? 'name-error' : null" | ||
| 30 | - /> | ||
| 31 | - <p *ngIf="errorOf('name') as message" class="field__error" id="name-error" role="alert">{{ message }}</p> | ||
| 32 | - </div> | ||
| 33 | - | ||
| 34 | - <div class="row"> | ||
| 35 | - <div class="field"> | ||
| 36 | - <label for="country">Pays</label> | ||
| 37 | <input | 10 | <input |
| 38 | - id="country" | 11 | + id="name" |
| 39 | type="text" | 12 | type="text" |
| 40 | - formControlName="country" | 13 | + formControlName="name" |
| 41 | - autocomplete="country-name" | 14 | + autocomplete="off" |
| 42 | - [attr.aria-describedby]="errorOf('country') ? 'country-error' : null" | 15 | + placeholder="Annecy, Col de l’Iseran…" |
| 16 | + [attr.aria-describedby]="errorOf('name') ? 'name-error' : null" | ||
| 43 | /> | 17 | /> |
| 44 | - <p *ngIf="errorOf('country') as message" class="field__error" id="country-error" role="alert">{{ message }}</p> | 18 | + </ui-field> |
| 45 | - </div> | ||
| 46 | 19 | ||
| 47 | - <div class="field"> | 20 | + <div class="row"> |
| 48 | - <label for="category">Type de lieu</label> | 21 | + <ui-field label="Pays" controlId="country" [error]="errorOf('country')"> |
| 49 | - <select id="category" formControlName="category"> | 22 | + <input |
| 50 | - <option *ngFor="let category of categories" [ngValue]="category">{{ categoryLabels[category] }}</option> | 23 | + id="country" |
| 51 | - </select> | 24 | + type="text" |
| 52 | - </div> | 25 | + formControlName="country" |
| 53 | - </div> | 26 | + autocomplete="country-name" |
| 27 | + [attr.aria-describedby]="errorOf('country') ? 'country-error' : null" | ||
| 28 | + /> | ||
| 29 | + </ui-field> | ||
| 54 | 30 | ||
| 55 | - <div class="row"> | 31 | + <ui-field label="Type de lieu" controlId="category"> |
| 56 | - <div class="field"> | 32 | + <select id="category" formControlName="category"> |
| 57 | - <label for="lat">Latitude</label> | 33 | + <option *ngFor="let category of categories" [ngValue]="category">{{ categoryLabels[category] }}</option> |
| 58 | - <input | 34 | + </select> |
| 59 | - id="lat" | 35 | + </ui-field> |
| 60 | - type="number" | ||
| 61 | - step="any" | ||
| 62 | - inputmode="decimal" | ||
| 63 | - formControlName="lat" | ||
| 64 | - [attr.aria-describedby]="errorOf('lat') ? 'lat-error' : null" | ||
| 65 | - /> | ||
| 66 | - <p *ngIf="errorOf('lat') as message" class="field__error" id="lat-error" role="alert">{{ message }}</p> | ||
| 67 | </div> | 36 | </div> |
| 68 | 37 | ||
| 69 | - <div class="field"> | 38 | + <div class="row"> |
| 70 | - <label for="lng">Longitude</label> | 39 | + <ui-field label="Latitude" controlId="lat" [error]="errorOf('lat')"> |
| 71 | - <input | 40 | + <input |
| 72 | - id="lng" | 41 | + id="lat" |
| 73 | - type="number" | 42 | + type="number" |
| 74 | - step="any" | 43 | + step="any" |
| 75 | - inputmode="decimal" | 44 | + inputmode="decimal" |
| 76 | - formControlName="lng" | 45 | + formControlName="lat" |
| 77 | - [attr.aria-describedby]="errorOf('lng') ? 'lng-error' : null" | 46 | + [attr.aria-describedby]="errorOf('lat') ? 'lat-error' : null" |
| 78 | - /> | 47 | + /> |
| 79 | - <p *ngIf="errorOf('lng') as message" class="field__error" id="lng-error" role="alert">{{ message }}</p> | 48 | + </ui-field> |
| 80 | - </div> | ||
| 81 | - </div> | ||
| 82 | - <p class="hint">Astuce : cliquez sur la carte pour remplir la latitude et la longitude.</p> | ||
| 83 | 49 | ||
| 84 | - <ng-container [ngSwitch]="status"> | 50 | + <ui-field label="Longitude" controlId="lng" [error]="errorOf('lng')"> |
| 85 | - <div class="row" *ngSwitchCase="'visited'"> | ||
| 86 | - <div class="field"> | ||
| 87 | - <label for="visitedOn">Date de visite</label> | ||
| 88 | <input | 51 | <input |
| 89 | - id="visitedOn" | 52 | + id="lng" |
| 90 | - type="date" | 53 | + type="number" |
| 91 | - formControlName="visitedOn" | 54 | + step="any" |
| 92 | - [attr.max]="maxDate" | 55 | + inputmode="decimal" |
| 93 | - [attr.aria-describedby]="errorOf('visitedOn') ? 'visitedOn-error' : null" | 56 | + formControlName="lng" |
| 57 | + [attr.aria-describedby]="errorOf('lng') ? 'lng-error' : null" | ||
| 94 | /> | 58 | /> |
| 95 | - <p *ngIf="errorOf('visitedOn') as message" class="field__error" id="visitedOn-error" role="alert">{{ message }}</p> | 59 | + </ui-field> |
| 96 | - </div> | 60 | + </div> |
| 61 | + <p class="hint">Astuce : cliquez sur la carte pour remplir la latitude et la longitude.</p> | ||
| 97 | 62 | ||
| 98 | - <div class="field"> | 63 | + <ng-container [ngSwitch]="status"> |
| 99 | - <label for="rating">Note (facultative)</label> | 64 | + <div class="row" *ngSwitchCase="'visited'"> |
| 100 | - <select id="rating" formControlName="rating"> | 65 | + <ui-field label="Date de visite" controlId="visitedOn" [error]="errorOf('visitedOn')"> |
| 101 | - <option [ngValue]="null">Pas de note</option> | 66 | + <input |
| 102 | - <option *ngFor="let rating of ratings" [ngValue]="rating">{{ rating }} sur 5</option> | 67 | + id="visitedOn" |
| 103 | - </select> | 68 | + type="date" |
| 69 | + formControlName="visitedOn" | ||
| 70 | + [attr.max]="maxDate" | ||
| 71 | + [attr.aria-describedby]="errorOf('visitedOn') ? 'visitedOn-error' : null" | ||
| 72 | + /> | ||
| 73 | + </ui-field> | ||
| 74 | + | ||
| 75 | + <ui-field label="Note (facultative)" controlId="rating"> | ||
| 76 | + <select id="rating" formControlName="rating"> | ||
| 77 | + <option [ngValue]="null">Pas de note</option> | ||
| 78 | + <option *ngFor="let rating of ratings" [ngValue]="rating">{{ rating }} sur 5</option> | ||
| 79 | + </select> | ||
| 80 | + </ui-field> | ||
| 104 | </div> | 81 | </div> |
| 105 | - </div> | 82 | + <ui-field *ngSwitchDefault label="Priorité" controlId="priority"> |
| 106 | - <div class="field" *ngSwitchDefault> | 83 | + <select id="priority" formControlName="priority"> |
| 107 | - <label for="priority">Priorité</label> | 84 | + <option *ngFor="let priority of priorities" [ngValue]="priority">{{ priorityLabels[priority] }}</option> |
| 108 | - <select id="priority" formControlName="priority"> | 85 | + </select> |
| 109 | - <option *ngFor="let priority of priorities" [ngValue]="priority">{{ priorityLabels[priority] }}</option> | 86 | + </ui-field> |
| 110 | - </select> | 87 | + </ng-container> |
| 111 | - </div> | ||
| 112 | - </ng-container> | ||
| 113 | 88 | ||
| 114 | - <div class="field"> | 89 | + <ui-field label="Note personnelle (facultative)" controlId="note" [error]="errorOf('note')"> |
| 115 | - <label for="note">Note personnelle (facultative)</label> | 90 | + <textarea |
| 116 | - <textarea | 91 | + id="note" |
| 117 | - id="note" | 92 | + rows="3" |
| 118 | - rows="3" | 93 | + formControlName="note" |
| 119 | - formControlName="note" | 94 | + [attr.aria-describedby]="errorOf('note') ? 'note-error' : null" |
| 120 | - [attr.aria-describedby]="errorOf('note') ? 'note-error' : null" | 95 | + ></textarea> |
| 121 | - ></textarea> | 96 | + </ui-field> |
| 122 | - <p *ngIf="errorOf('note') as message" class="field__error" id="note-error" role="alert">{{ message }}</p> | ||
| 123 | - </div> | ||
| 124 | 97 | ||
| 125 | - <div class="actions"> | 98 | + <div class="actions"> |
| 126 | - <button type="submit" class="btn btn--primary">Enregistrer</button> | 99 | + <button type="submit" ui-button variant="primary">Enregistrer</button> |
| 127 | - <a class="btn" routerLink="/resume">Annuler</a> | 100 | + <a ui-button routerLink="/resume">Annuler</a> |
| 128 | - </div> | 101 | + </div> |
| 129 | - </form> | 102 | + </form> |
| 103 | + </ui-card> | ||
| 130 | 104 | ||
| 131 | - <section class="card sticky" aria-labelledby="picker-title"> | 105 | + <ui-card class="sticky" aria-labelledby="picker-title"> |
| 132 | <h2 id="picker-title">Placer le lieu sur la carte</h2> | 106 | <h2 id="picker-title">Placer le lieu sur la carte</h2> |
| 133 | <div class="map-slot"> | 107 | <div class="map-slot"> |
| 134 | <app-travel-map | 108 | <app-travel-map |
| @@ -139,6 +113,6 @@ | |||
| 139 | (locationPicked)="onPick($event)" | 113 | (locationPicked)="onPick($event)" |
| 140 | ></app-travel-map> | 114 | ></app-travel-map> |
| 141 | </div> | 115 | </div> |
| 142 | - </section> | 116 | + </ui-card> |
| 143 | </div> | 117 | </div> |
| 144 | </section> | 118 | </section> |
src/app/travel/pages/place-form-page/place-form-page.component.spec.ts +1 -1 | @@ -68,7 +68,7 @@ describe('PlaceFormPageComponent', () => { | ||
| 68 | 68 | |
| 69 | 69 | expect(store.places().length).toBe(0); |
| 70 | 70 | expect(navigate).not.toHaveBeenCalled(); |
| 71 | - expect(root.querySelectorAll('.field__error').length).toBeGreaterThanOrEqual(4); | |
| 71 | + expect(root.querySelectorAll('.ui-field__error').length).toBeGreaterThanOrEqual(4); | |
| 72 | 72 | }); |
| 73 | 73 | |
| 74 | 74 | it('refuse une latitude hors limites', () => { |
| @@ -68,7 +68,7 @@ describe('PlaceFormPageComponent', () => { | |||
| 68 | 68 | ||
| 69 | expect(store.places().length).toBe(0); | 69 | expect(store.places().length).toBe(0); |
| 70 | expect(navigate).not.toHaveBeenCalled(); | 70 | expect(navigate).not.toHaveBeenCalled(); |
| 71 | - expect(root.querySelectorAll('.field__error').length).toBeGreaterThanOrEqual(4); | 71 | + expect(root.querySelectorAll('.ui-field__error').length).toBeGreaterThanOrEqual(4); |
| 72 | }); | 72 | }); |
| 73 | 73 | ||
| 74 | it('refuse une latitude hors limites', () => { | 74 | it('refuse une latitude hors limites', () => { |
src/app/travel/pages/place-form-page/place-form-page.component.ts +23 -1 | @@ -2,6 +2,14 @@ import { CommonModule } from '@angular/common'; | ||
| 2 | 2 | import { Component, OnInit } from '@angular/core'; |
| 3 | 3 | import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; |
| 4 | 4 | import { Router, RouterLink } from '@angular/router'; |
| 5 | +import { | |
| 6 | + UiButtonComponent, | |
| 7 | + UiCardComponent, | |
| 8 | + UiFieldComponent, | |
| 9 | + UiOption, | |
| 10 | + UiPageHeaderComponent, | |
| 11 | + UiSegmentedComponent, | |
| 12 | +} from 'ui-kit'; | |
| 5 | 13 | import { TravelMapComponent } from '../../components/travel-map/travel-map.component'; |
| 6 | 14 | import { |
| 7 | 15 | CATEGORIES, |
| @@ -22,7 +30,17 @@ type TextField = 'name' | 'country' | 'lat' | 'lng' | 'visitedOn' | 'note'; | ||
| 22 | 30 | @Component({ |
| 23 | 31 | selector: 'app-place-form-page', |
| 24 | 32 | standalone: true, |
| 25 | - imports: [CommonModule, ReactiveFormsModule, RouterLink, TravelMapComponent], | |
| 33 | + imports: [ | |
| 34 | + CommonModule, | |
| 35 | + ReactiveFormsModule, | |
| 36 | + RouterLink, | |
| 37 | + TravelMapComponent, | |
| 38 | + UiButtonComponent, | |
| 39 | + UiCardComponent, | |
| 40 | + UiFieldComponent, | |
| 41 | + UiPageHeaderComponent, | |
| 42 | + UiSegmentedComponent, | |
| 43 | + ], | |
| 26 | 44 | templateUrl: './place-form-page.component.html', |
| 27 | 45 | }) |
| 28 | 46 | export class PlaceFormPageComponent implements OnInit { |
| @@ -32,6 +50,10 @@ export class PlaceFormPageComponent implements OnInit { | ||
| 32 | 50 | priorityLabels = PRIORITY_LABELS; |
| 33 | 51 | ratings = [1, 2, 3, 4, 5]; |
| 34 | 52 | maxDate = todayIso(); |
| 53 | + statusOptions: UiOption<PlaceStatus>[] = [ | |
| 54 | + { value: 'visited', label: 'Déjà visité' }, | |
| 55 | + { value: 'wishlist', label: 'Sur ma liste de souhaits' }, | |
| 56 | + ]; | |
| 35 | 57 | |
| 36 | 58 | status: PlaceStatus = 'visited'; |
| 37 | 59 | /** Point à afficher sur la carte, tant que la latitude et la longitude saisies sont valides. */ |
| @@ -2,6 +2,14 @@ import { CommonModule } from '@angular/common'; | |||
| 2 | import { Component, OnInit } from '@angular/core'; | 2 | import { Component, OnInit } from '@angular/core'; |
| 3 | import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; | 3 | import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; |
| 4 | import { Router, RouterLink } from '@angular/router'; | 4 | import { Router, RouterLink } from '@angular/router'; |
| 5 | +import { | ||
| 6 | + UiButtonComponent, | ||
| 7 | + UiCardComponent, | ||
| 8 | + UiFieldComponent, | ||
| 9 | + UiOption, | ||
| 10 | + UiPageHeaderComponent, | ||
| 11 | + UiSegmentedComponent, | ||
| 12 | +} from 'ui-kit'; | ||
| 5 | import { TravelMapComponent } from '../../components/travel-map/travel-map.component'; | 13 | import { TravelMapComponent } from '../../components/travel-map/travel-map.component'; |
| 6 | import { | 14 | import { |
| 7 | CATEGORIES, | 15 | CATEGORIES, |
| @@ -22,7 +30,17 @@ type TextField = 'name' | 'country' | 'lat' | 'lng' | 'visitedOn' | 'note'; | |||
| 22 | @Component({ | 30 | @Component({ |
| 23 | selector: 'app-place-form-page', | 31 | selector: 'app-place-form-page', |
| 24 | standalone: true, | 32 | standalone: true, |
| 25 | - imports: [CommonModule, ReactiveFormsModule, RouterLink, TravelMapComponent], | 33 | + imports: [ |
| 34 | + CommonModule, | ||
| 35 | + ReactiveFormsModule, | ||
| 36 | + RouterLink, | ||
| 37 | + TravelMapComponent, | ||
| 38 | + UiButtonComponent, | ||
| 39 | + UiCardComponent, | ||
| 40 | + UiFieldComponent, | ||
| 41 | + UiPageHeaderComponent, | ||
| 42 | + UiSegmentedComponent, | ||
| 43 | + ], | ||
| 26 | templateUrl: './place-form-page.component.html', | 44 | templateUrl: './place-form-page.component.html', |
| 27 | }) | 45 | }) |
| 28 | export class PlaceFormPageComponent implements OnInit { | 46 | export class PlaceFormPageComponent implements OnInit { |
| @@ -32,6 +50,10 @@ export class PlaceFormPageComponent implements OnInit { | |||
| 32 | priorityLabels = PRIORITY_LABELS; | 50 | priorityLabels = PRIORITY_LABELS; |
| 33 | ratings = [1, 2, 3, 4, 5]; | 51 | ratings = [1, 2, 3, 4, 5]; |
| 34 | maxDate = todayIso(); | 52 | maxDate = todayIso(); |
| 53 | + statusOptions: UiOption<PlaceStatus>[] = [ | ||
| 54 | + { value: 'visited', label: 'Déjà visité' }, | ||
| 55 | + { value: 'wishlist', label: 'Sur ma liste de souhaits' }, | ||
| 56 | + ]; | ||
| 35 | 57 | ||
| 36 | status: PlaceStatus = 'visited'; | 58 | status: PlaceStatus = 'visited'; |
| 37 | /** Point à afficher sur la carte, tant que la latitude et la longitude saisies sont valides. */ | 59 | /** Point à afficher sur la carte, tant que la latitude et la longitude saisies sont valides. */ |
src/app/travel/pages/summary-page/summary-page.component.html +18 -28 | @@ -1,43 +1,33 @@ | ||
| 1 | 1 | <section class="page"> |
| 2 | - <header class="page__header"> | |
| 3 | - <p class="eyebrow">Carnet de voyage</p> | |
| 4 | - <h1>Votre monde en un coup d’œil</h1> | |
| 5 | - </header> | |
| 2 | + <ui-page-header eyebrow="Carnet de voyage" heading="Votre monde en un coup d’œil"></ui-page-header> | |
| 6 | 3 | |
| 7 | 4 | <ul class="stat-grid" aria-label="Statistiques de voyage"> |
| 8 | - <li class="card stat"> | |
| 9 | - <span class="stat__value">{{ store.stats().visitedCount }}</span> | |
| 10 | - <span class="stat__label">{{ store.stats().visitedCount > 1 ? 'lieux visités' : 'lieu visité' }}</span> | |
| 5 | + <li> | |
| 6 | + <ui-stat [value]="store.stats().visitedCount" [label]="store.stats().visitedCount > 1 ? 'lieux visités' : 'lieu visité'"></ui-stat> | |
| 11 | 7 | </li> |
| 12 | - <li class="card stat"> | |
| 13 | - <span class="stat__value">{{ store.stats().countryCount }}</span> | |
| 14 | - <span class="stat__label">pays</span> | |
| 8 | + <li> | |
| 9 | + <ui-stat [value]="store.stats().countryCount" label="pays"></ui-stat> | |
| 15 | 10 | </li> |
| 16 | - <li class="card stat"> | |
| 17 | - <span class="stat__value">{{ store.stats().averageRating ?? '—' }}</span> | |
| 18 | - <span class="stat__label">note moyenne</span> | |
| 11 | + <li> | |
| 12 | + <ui-stat [value]="store.stats().averageRating ?? '—'" label="note moyenne"></ui-stat> | |
| 19 | 13 | </li> |
| 20 | - <li class="card stat"> | |
| 21 | - <span class="stat__value">{{ store.stats().wishlistCount }}</span> | |
| 22 | - <span class="stat__label">à découvrir</span> | |
| 14 | + <li> | |
| 15 | + <ui-stat [value]="store.stats().wishlistCount" label="à découvrir"></ui-stat> | |
| 23 | 16 | </li> |
| 24 | 17 | </ul> |
| 25 | 18 | |
| 26 | 19 | <div class="layout layout--map-first"> |
| 27 | - <section class="card" aria-labelledby="map-title"> | |
| 28 | - <div class="card__head"> | |
| 20 | + <ui-card aria-labelledby="map-title"> | |
| 21 | + <div class="card-head"> | |
The old legend list had aria-label="Légende" on the . Unless UiLegendComponent sets an accessible label, the legend loses that grouping announcement. Worth double-checking the ui-kit component. | ||
| 29 | 22 | <h2 id="map-title">Carte</h2> |
| 30 | - <ul class="legend" aria-label="Légende"> | |
| 31 | - <li><span class="legend__dot legend__dot--visited" aria-hidden="true"></span>Visité</li> | |
| 32 | - <li><span class="legend__dot legend__dot--wishlist" aria-hidden="true"></span>À visiter</li> | |
| 33 | - </ul> | |
| 23 | + <ui-legend [items]="legend"></ui-legend> | |
| 34 | 24 | </div> |
| 35 | 25 | <div class="map-slot"> |
| 36 | 26 | <app-travel-map [places]="store.places()" label="Carte de vos lieux visités et de vos souhaits"></app-travel-map> |
| 37 | 27 | </div> |
| 38 | - </section> | |
| 28 | + </ui-card> | |
| 39 | 29 | |
| 40 | - <section class="card" aria-labelledby="recent-title"> | |
| 30 | + <ui-card aria-labelledby="recent-title"> | |
| 41 | 31 | <h2 id="recent-title">Derniers voyages</h2> |
| 42 | 32 | |
| 43 | 33 | <ol class="timeline" *ngIf="recent().length > 0; else noTrip"> |
| @@ -48,18 +38,18 @@ | ||
| 48 | 38 | </div> |
| 49 | 39 | <div class="timeline__meta"> |
| 50 | 40 | <time [attr.datetime]="place.visitedOn">{{ place.visitedOn | date: 'longDate' }}</time> |
| 51 | - <app-rating [value]="place.rating"></app-rating> | |
| 41 | + <ui-rating [value]="place.rating"></ui-rating> | |
| 52 | 42 | </div> |
| 53 | 43 | <p class="timeline__note" *ngIf="place.note">{{ place.note }}</p> |
| 54 | 44 | </li> |
| 55 | 45 | </ol> |
| 56 | 46 | |
| 57 | 47 | <ng-template #noTrip> |
| 58 | - <p class="empty"> | |
| 48 | + <ui-empty-state> | |
| 59 | 49 | Aucun lieu visité pour l’instant. |
| 60 | 50 | <a routerLink="/lieux/nouveau">Ajoutez votre premier voyage</a>. |
| 61 | - </p> | |
| 51 | + </ui-empty-state> | |
| 62 | 52 | </ng-template> |
| 63 | - </section> | |
| 53 | + </ui-card> | |
| 64 | 54 | </div> |
| 65 | 55 | </section> |
| @@ -1,43 +1,33 @@ | |||
| 1 | <section class="page"> | 1 | <section class="page"> |
| 2 | - <header class="page__header"> | 2 | + <ui-page-header eyebrow="Carnet de voyage" heading="Votre monde en un coup d’œil"></ui-page-header> |
| 3 | - <p class="eyebrow">Carnet de voyage</p> | ||
| 4 | - <h1>Votre monde en un coup d’œil</h1> | ||
| 5 | - </header> | ||
| 6 | 3 | ||
| 7 | <ul class="stat-grid" aria-label="Statistiques de voyage"> | 4 | <ul class="stat-grid" aria-label="Statistiques de voyage"> |
| 8 | - <li class="card stat"> | 5 | + <li> |
| 9 | - <span class="stat__value">{{ store.stats().visitedCount }}</span> | 6 | + <ui-stat [value]="store.stats().visitedCount" [label]="store.stats().visitedCount > 1 ? 'lieux visités' : 'lieu visité'"></ui-stat> |
| 10 | - <span class="stat__label">{{ store.stats().visitedCount > 1 ? 'lieux visités' : 'lieu visité' }}</span> | ||
| 11 | </li> | 7 | </li> |
| 12 | - <li class="card stat"> | 8 | + <li> |
| 13 | - <span class="stat__value">{{ store.stats().countryCount }}</span> | 9 | + <ui-stat [value]="store.stats().countryCount" label="pays"></ui-stat> |
| 14 | - <span class="stat__label">pays</span> | ||
| 15 | </li> | 10 | </li> |
| 16 | - <li class="card stat"> | 11 | + <li> |
| 17 | - <span class="stat__value">{{ store.stats().averageRating ?? '—' }}</span> | 12 | + <ui-stat [value]="store.stats().averageRating ?? '—'" label="note moyenne"></ui-stat> |
| 18 | - <span class="stat__label">note moyenne</span> | ||
| 19 | </li> | 13 | </li> |
| 20 | - <li class="card stat"> | 14 | + <li> |
| 21 | - <span class="stat__value">{{ store.stats().wishlistCount }}</span> | 15 | + <ui-stat [value]="store.stats().wishlistCount" label="à découvrir"></ui-stat> |
| 22 | - <span class="stat__label">à découvrir</span> | ||
| 23 | </li> | 16 | </li> |
| 24 | </ul> | 17 | </ul> |
| 25 | 18 | ||
| 26 | <div class="layout layout--map-first"> | 19 | <div class="layout layout--map-first"> |
| 27 | - <section class="card" aria-labelledby="map-title"> | 20 | + <ui-card aria-labelledby="map-title"> |
| 28 | - <div class="card__head"> | 21 | + <div class="card-head"> |
| 29 | <h2 id="map-title">Carte</h2> | 22 | <h2 id="map-title">Carte</h2> |
| 30 | - <ul class="legend" aria-label="Légende"> | 23 | + <ui-legend [items]="legend"></ui-legend> |
| 31 | - <li><span class="legend__dot legend__dot--visited" aria-hidden="true"></span>Visité</li> | ||
| 32 | - <li><span class="legend__dot legend__dot--wishlist" aria-hidden="true"></span>À visiter</li> | ||
| 33 | - </ul> | ||
| 34 | </div> | 24 | </div> |
| 35 | <div class="map-slot"> | 25 | <div class="map-slot"> |
| 36 | <app-travel-map [places]="store.places()" label="Carte de vos lieux visités et de vos souhaits"></app-travel-map> | 26 | <app-travel-map [places]="store.places()" label="Carte de vos lieux visités et de vos souhaits"></app-travel-map> |
| 37 | </div> | 27 | </div> |
| 38 | - </section> | 28 | + </ui-card> |
| 39 | 29 | ||
| 40 | - <section class="card" aria-labelledby="recent-title"> | 30 | + <ui-card aria-labelledby="recent-title"> |
| 41 | <h2 id="recent-title">Derniers voyages</h2> | 31 | <h2 id="recent-title">Derniers voyages</h2> |
| 42 | 32 | ||
| 43 | <ol class="timeline" *ngIf="recent().length > 0; else noTrip"> | 33 | <ol class="timeline" *ngIf="recent().length > 0; else noTrip"> |
| @@ -48,18 +38,18 @@ | |||
| 48 | </div> | 38 | </div> |
| 49 | <div class="timeline__meta"> | 39 | <div class="timeline__meta"> |
| 50 | <time [attr.datetime]="place.visitedOn">{{ place.visitedOn | date: 'longDate' }}</time> | 40 | <time [attr.datetime]="place.visitedOn">{{ place.visitedOn | date: 'longDate' }}</time> |
| 51 | - <app-rating [value]="place.rating"></app-rating> | 41 | + <ui-rating [value]="place.rating"></ui-rating> |
| 52 | </div> | 42 | </div> |
| 53 | <p class="timeline__note" *ngIf="place.note">{{ place.note }}</p> | 43 | <p class="timeline__note" *ngIf="place.note">{{ place.note }}</p> |
| 54 | </li> | 44 | </li> |
| 55 | </ol> | 45 | </ol> |
| 56 | 46 | ||
| 57 | <ng-template #noTrip> | 47 | <ng-template #noTrip> |
| 58 | - <p class="empty"> | 48 | + <ui-empty-state> |
| 59 | Aucun lieu visité pour l’instant. | 49 | Aucun lieu visité pour l’instant. |
| 60 | <a routerLink="/lieux/nouveau">Ajoutez votre premier voyage</a>. | 50 | <a routerLink="/lieux/nouveau">Ajoutez votre premier voyage</a>. |
| 61 | - </p> | 51 | + </ui-empty-state> |
| 62 | </ng-template> | 52 | </ng-template> |
| 63 | - </section> | 53 | + </ui-card> |
| 64 | </div> | 54 | </div> |
| 65 | </section> | 55 | </section> |
src/app/travel/pages/summary-page/summary-page.component.spec.ts +2 -2 | @@ -21,7 +21,7 @@ describe('SummaryPageComponent', () => { | ||
| 21 | 21 | |
| 22 | 22 | it('affiche les statistiques, la carte et les derniers voyages', async () => { |
| 23 | 23 | const root = await render(); |
| 24 | - const values = Array.from(root.querySelectorAll('.stat__value')).map((el) => el.textContent?.trim()); | |
| 24 | + const values = Array.from(root.querySelectorAll('ui-stat .value')).map((el) => el.textContent?.trim()); | |
The selector 'ui-stat .value' now depends on an internal class of the ui-kit component. If ui-kit ever renames that class, this test breaks even though the component contract ([value]/[label] inputs) is unchanged. Querying 'ui-stat' elements and reading their text, or exposing a stable hook (e.g. data-testid or a host class), would be less brittle. | ||
| 25 | 25 | |
| 26 | 26 | expect(values).toEqual(['5', '5', '4.6', '5']); |
| 27 | 27 | expect(root.querySelector('app-travel-map')).not.toBeNull(); |
| @@ -32,7 +32,7 @@ describe('SummaryPageComponent', () => { | ||
| 32 | 32 | it('invite à ajouter un premier voyage quand rien n’est enregistré', async () => { |
| 33 | 33 | const root = await render('[]'); |
| 34 | 34 | |
| 35 | - expect(root.querySelector('.empty a')?.getAttribute('href')).toBe('/lieux/nouveau'); | |
| 35 | + expect(root.querySelector('ui-empty-state a')?.getAttribute('href')).toBe('/lieux/nouveau'); | |
| 36 | 36 | expect(root.querySelector('.timeline')).toBeNull(); |
| 37 | 37 | }); |
| 38 | 38 | }); |
| @@ -21,7 +21,7 @@ describe('SummaryPageComponent', () => { | |||
| 21 | 21 | ||
| 22 | it('affiche les statistiques, la carte et les derniers voyages', async () => { | 22 | it('affiche les statistiques, la carte et les derniers voyages', async () => { |
| 23 | const root = await render(); | 23 | const root = await render(); |
| 24 | - const values = Array.from(root.querySelectorAll('.stat__value')).map((el) => el.textContent?.trim()); | 24 | + const values = Array.from(root.querySelectorAll('ui-stat .value')).map((el) => el.textContent?.trim()); |
| 25 | 25 | ||
| 26 | expect(values).toEqual(['5', '5', '4.6', '5']); | 26 | expect(values).toEqual(['5', '5', '4.6', '5']); |
| 27 | expect(root.querySelector('app-travel-map')).not.toBeNull(); | 27 | expect(root.querySelector('app-travel-map')).not.toBeNull(); |
| @@ -32,7 +32,7 @@ describe('SummaryPageComponent', () => { | |||
| 32 | it('invite à ajouter un premier voyage quand rien n’est enregistré', async () => { | 32 | it('invite à ajouter un premier voyage quand rien n’est enregistré', async () => { |
| 33 | const root = await render('[]'); | 33 | const root = await render('[]'); |
| 34 | 34 | ||
| 35 | - expect(root.querySelector('.empty a')?.getAttribute('href')).toBe('/lieux/nouveau'); | 35 | + expect(root.querySelector('ui-empty-state a')?.getAttribute('href')).toBe('/lieux/nouveau'); |
| 36 | expect(root.querySelector('.timeline')).toBeNull(); | 36 | expect(root.querySelector('.timeline')).toBeNull(); |
| 37 | }); | 37 | }); |
| 38 | }); | 38 | }); |
src/app/travel/pages/summary-page/summary-page.component.ts +24 -2 src/app/travel/pages/wishlist-page/wishlist-page.component.html +16 -18 src/app/travel/pages/wishlist-page/wishlist-page.component.spec.ts +2 -2 src/app/travel/pages/wishlist-page/wishlist-page.component.ts +19 -1 src/styles.css +14 -229
The color token changes from --on-accent to --ui-on-accent. Make sure this variable is actually provided by ui-kit's styles (and that those styles are loaded globally), otherwise the active nav link will fall back to an inherited/inherited-contrast color.