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 | @@ -1,7 +1,15 @@ | ||
| 1 | 1 | import { CommonModule } from '@angular/common'; |
| 2 | 2 | import { Component, Signal, computed } from '@angular/core'; |
| 3 | 3 | import { RouterLink } from '@angular/router'; |
| 4 | -import { RatingComponent } from '../../components/rating/rating.component'; | |
| 4 | +import { | |
| 5 | + UiCardComponent, | |
| 6 | + UiEmptyStateComponent, | |
| 7 | + UiLegendComponent, | |
| 8 | + UiLegendItem, | |
| 9 | + UiPageHeaderComponent, | |
| 10 | + UiRatingComponent, | |
| 11 | + UiStatComponent, | |
| 12 | +} from 'ui-kit'; | |
| 5 | 13 | import { TravelMapComponent } from '../../components/travel-map/travel-map.component'; |
| 6 | 14 | import { CATEGORY_LABELS, Place } from '../../models/place.model'; |
| 7 | 15 | import { TravelStore } from '../../services/travel-store.service'; |
| @@ -9,11 +17,25 @@ import { TravelStore } from '../../services/travel-store.service'; | ||
| 9 | 17 | @Component({ |
| 10 | 18 | selector: 'app-summary-page', |
| 11 | 19 | standalone: true, |
| 12 | - imports: [CommonModule, RouterLink, RatingComponent, TravelMapComponent], | |
| 20 | + imports: [ | |
| 21 | + CommonModule, | |
| 22 | + RouterLink, | |
| 23 | + TravelMapComponent, | |
| 24 | + UiCardComponent, | |
| 25 | + UiEmptyStateComponent, | |
| 26 | + UiLegendComponent, | |
| 27 | + UiPageHeaderComponent, | |
| 28 | + UiRatingComponent, | |
| 29 | + UiStatComponent, | |
| 30 | + ], | |
| 13 | 31 | templateUrl: './summary-page.component.html', |
| 14 | 32 | }) |
| 15 | 33 | export class SummaryPageComponent { |
| 16 | 34 | categoryLabels = CATEGORY_LABELS; |
| 35 | + legend: UiLegendItem[] = [ | |
| 36 | + { kind: 'primary', label: 'Visité' }, | |
| 37 | + { kind: 'secondary', label: 'À visiter' }, | |
| 38 | + ]; | |
| 17 | 39 | recent: Signal<Place[]>; |
| 18 | 40 | |
| 19 | 41 | constructor(public store: TravelStore) { |
| @@ -1,7 +1,15 @@ | |||
| 1 | import { CommonModule } from '@angular/common'; | 1 | import { CommonModule } from '@angular/common'; |
| 2 | import { Component, Signal, computed } from '@angular/core'; | 2 | import { Component, Signal, computed } from '@angular/core'; |
| 3 | import { RouterLink } from '@angular/router'; | 3 | import { RouterLink } from '@angular/router'; |
| 4 | -import { RatingComponent } from '../../components/rating/rating.component'; | 4 | +import { |
| 5 | + UiCardComponent, | ||
| 6 | + UiEmptyStateComponent, | ||
| 7 | + UiLegendComponent, | ||
| 8 | + UiLegendItem, | ||
| 9 | + UiPageHeaderComponent, | ||
| 10 | + UiRatingComponent, | ||
| 11 | + UiStatComponent, | ||
| 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 { CATEGORY_LABELS, Place } from '../../models/place.model'; | 14 | import { CATEGORY_LABELS, Place } from '../../models/place.model'; |
| 7 | import { TravelStore } from '../../services/travel-store.service'; | 15 | import { TravelStore } from '../../services/travel-store.service'; |
| @@ -9,11 +17,25 @@ import { TravelStore } from '../../services/travel-store.service'; | |||
| 9 | @Component({ | 17 | @Component({ |
| 10 | selector: 'app-summary-page', | 18 | selector: 'app-summary-page', |
| 11 | standalone: true, | 19 | standalone: true, |
| 12 | - imports: [CommonModule, RouterLink, RatingComponent, TravelMapComponent], | 20 | + imports: [ |
| 21 | + CommonModule, | ||
| 22 | + RouterLink, | ||
| 23 | + TravelMapComponent, | ||
| 24 | + UiCardComponent, | ||
| 25 | + UiEmptyStateComponent, | ||
| 26 | + UiLegendComponent, | ||
| 27 | + UiPageHeaderComponent, | ||
| 28 | + UiRatingComponent, | ||
| 29 | + UiStatComponent, | ||
| 30 | + ], | ||
| 13 | templateUrl: './summary-page.component.html', | 31 | templateUrl: './summary-page.component.html', |
| 14 | }) | 32 | }) |
| 15 | export class SummaryPageComponent { | 33 | export class SummaryPageComponent { |
| 16 | categoryLabels = CATEGORY_LABELS; | 34 | categoryLabels = CATEGORY_LABELS; |
| 35 | + legend: UiLegendItem[] = [ | ||
| 36 | + { kind: 'primary', label: 'Visité' }, | ||
| 37 | + { kind: 'secondary', label: 'À visiter' }, | ||
| 38 | + ]; | ||
| 17 | recent: Signal<Place[]>; | 39 | recent: Signal<Place[]>; |
| 18 | 40 | ||
| 19 | constructor(public store: TravelStore) { | 41 | constructor(public store: TravelStore) { |
src/app/travel/pages/wishlist-page/wishlist-page.component.html +16 -18 | @@ -1,17 +1,14 @@ | ||
| 1 | 1 | <section class="page"> |
| 2 | - <header class="page__header"> | |
| 3 | - <p class="eyebrow">Liste de souhaits</p> | |
| 4 | - <h1>Là où vous voulez aller</h1> | |
| 5 | - </header> | |
| 2 | + <ui-page-header eyebrow="Liste de souhaits" heading="Là où vous voulez aller"></ui-page-header> | |
| 6 | 3 | |
| 7 | - <p class="notice" role="status" aria-live="polite"> | |
| 4 | + <ui-notice> | |
| 8 | 5 | <ng-container *ngIf="justVisited"> |
Resolved The old notice element carried role="status" aria-live="polite" so screen readers announced '{{ justVisited }} rejoint vos lieux visités' when it appeared. Unless UiNoticeComponent sets an equivalent live region internally (not visible here), this accessibility behavior is lost. Please verify the component exposes a live region, or keep the attributes on this instance. The old carried role="status" and aria-live="polite" so screen readers announce when a wishlist item is marked visited. The bare replacement drops those semantics (unless UiNoticeComponent itself sets them, which the diff doesn't show). Please preserve the live-region announcement, either on the component or via . | ||
| 9 | 6 | {{ justVisited }} rejoint vos lieux visités. <a routerLink="/resume">Voir le résumé</a> |
| 10 | 7 | </ng-container> |
| 11 | - </p> | |
| 8 | + </ui-notice> | |
| 12 | 9 | |
| 13 | 10 | <div class="layout"> |
| 14 | - <section class="card" aria-labelledby="wishlist-title"> | |
| 11 | + <ui-card aria-labelledby="wishlist-title"> | |
| 15 | 12 | <h2 id="wishlist-title"> |
| 16 | 13 | {{ store.wishlist().length }} {{ store.wishlist().length > 1 ? 'destinations' : 'destination' }} |
| 17 | 14 | </h2> |
| @@ -20,9 +17,9 @@ | ||
| 20 | 17 | <li class="wish" *ngFor="let place of store.wishlist()"> |
| 21 | 18 | <div class="wish__head"> |
| 22 | 19 | <strong>{{ place.name }}</strong> |
| 23 | - <span class="badge" [ngClass]="'badge--' + (place.priority || 'medium')"> | |
| 20 | + <ui-badge [tone]="place.priority === 'high' ? 'warm' : 'accent'"> | |
| 24 | 21 | {{ priorityLabels[place.priority || 'medium'] }} |
| 25 | - </span> | |
| 22 | + </ui-badge> | |
Resolved The badge tone mapping ('high' → 'warm', everything else → 'accent') collapses 'medium' and 'low' priorities into the same visual style, whereas the old ngClass produced distinct badge--medium and badge--low variants. If distinguishing low from medium priorities still matters visually, consider a third tone or a fallback. | ||
| 26 | 23 | </div> |
The badge tone mapping collapses three priorities into two tones: 'high' → 'warm' but both 'medium' and 'low' → 'accent'. The old badge--{priority} classes distinguished low from medium visually. If the ui-kit palette has a neutral/soft tone, consider mapping 'low' to it so users can still tell them apart at a glance. | ||
| 27 | 24 | <span class="muted">{{ place.country }} · {{ categoryLabels[place.category] }}</span> |
| 28 | 25 | <p class="wish__note" *ngIf="place.note">{{ place.note }}</p> |
| @@ -30,7 +27,7 @@ | ||
| 30 | 27 | <div class="actions"> |
| 31 | 28 | <button |
| 32 | 29 | type="button" |
| 33 | - class="btn btn--primary btn--sm" | |
| 30 | + ui-button variant="primary" size="sm" | |
| 34 | 31 | [attr.aria-label]="'Marquer ' + place.name + ' comme visité'" |
| 35 | 32 | (click)="markVisited(place)" |
| 36 | 33 | > |
| @@ -40,18 +37,19 @@ | ||
| 40 | 37 | <ng-container *ngIf="pendingRemoval === place.id; else removeButton"> |
| 41 | 38 | <button |
| 42 | 39 | type="button" |
| 43 | - class="btn btn--danger btn--sm" | |
| 40 | + ui-button variant="danger" size="sm" | |
| 44 | 41 | [attr.aria-label]="'Confirmer la suppression de ' + place.name" |
| 45 | 42 | (click)="confirmRemoval(place)" |
| 46 | 43 | > |
| 47 | 44 | Confirmer |
| 48 | 45 | </button> |
| 49 | - <button type="button" class="btn btn--sm" (click)="cancelRemoval()">Annuler</button> | |
| 46 | + <button type="button" ui-button size="sm" (click)="cancelRemoval()">Annuler</button> | |
| 50 | 47 | </ng-container> |
| 51 | 48 | <ng-template #removeButton> |
| 52 | 49 | <button |
| 53 | 50 | type="button" |
| 54 | - class="btn btn--sm" | |
| 51 | + ui-button | |
| 52 | + size="sm" | |
| 55 | 53 | [attr.aria-label]="'Supprimer ' + place.name" |
| 56 | 54 | (click)="askRemoval(place)" |
| 57 | 55 | > |
| @@ -63,13 +61,13 @@ | ||
| 63 | 61 | </ul> |
| 64 | 62 | |
| 65 | 63 | <ng-template #emptyList> |
| 66 | - <p class="empty"> | |
| 64 | + <ui-empty-state> | |
| 67 | 65 | Votre liste est vide. <a routerLink="/lieux/nouveau">Ajoutez une destination</a> qui vous fait rêver. |
| 68 | - </p> | |
| 66 | + </ui-empty-state> | |
| 69 | 67 | </ng-template> |
| 70 | - </section> | |
| 68 | + </ui-card> | |
| 71 | 69 | |
| 72 | - <section class="card" aria-labelledby="wish-map-title"> | |
| 70 | + <ui-card aria-labelledby="wish-map-title"> | |
| 73 | 71 | <h2 id="wish-map-title">Sur la carte</h2> |
| 74 | 72 | <div class="map-slot"> |
| 75 | 73 | <app-travel-map |
| @@ -77,6 +75,6 @@ | ||
| 77 | 75 | label="Carte des destinations de votre liste de souhaits" |
| 78 | 76 | ></app-travel-map> |
| 79 | 77 | </div> |
| 80 | - </section> | |
| 78 | + </ui-card> | |
| 81 | 79 | </div> |
| 82 | 80 | </section> |
| @@ -1,17 +1,14 @@ | |||
| 1 | <section class="page"> | 1 | <section class="page"> |
| 2 | - <header class="page__header"> | 2 | + <ui-page-header eyebrow="Liste de souhaits" heading="Là où vous voulez aller"></ui-page-header> |
| 3 | - <p class="eyebrow">Liste de souhaits</p> | ||
| 4 | - <h1>Là où vous voulez aller</h1> | ||
| 5 | - </header> | ||
| 6 | 3 | ||
| 7 | - <p class="notice" role="status" aria-live="polite"> | 4 | + <ui-notice> |
| 8 | <ng-container *ngIf="justVisited"> | 5 | <ng-container *ngIf="justVisited"> |
| 9 | {{ justVisited }} rejoint vos lieux visités. <a routerLink="/resume">Voir le résumé</a> | 6 | {{ justVisited }} rejoint vos lieux visités. <a routerLink="/resume">Voir le résumé</a> |
| 10 | </ng-container> | 7 | </ng-container> |
| 11 | - </p> | 8 | + </ui-notice> |
| 12 | 9 | ||
| 13 | <div class="layout"> | 10 | <div class="layout"> |
| 14 | - <section class="card" aria-labelledby="wishlist-title"> | 11 | + <ui-card aria-labelledby="wishlist-title"> |
| 15 | <h2 id="wishlist-title"> | 12 | <h2 id="wishlist-title"> |
| 16 | {{ store.wishlist().length }} {{ store.wishlist().length > 1 ? 'destinations' : 'destination' }} | 13 | {{ store.wishlist().length }} {{ store.wishlist().length > 1 ? 'destinations' : 'destination' }} |
| 17 | </h2> | 14 | </h2> |
| @@ -20,9 +17,9 @@ | |||
| 20 | <li class="wish" *ngFor="let place of store.wishlist()"> | 17 | <li class="wish" *ngFor="let place of store.wishlist()"> |
| 21 | <div class="wish__head"> | 18 | <div class="wish__head"> |
| 22 | <strong>{{ place.name }}</strong> | 19 | <strong>{{ place.name }}</strong> |
| 23 | - <span class="badge" [ngClass]="'badge--' + (place.priority || 'medium')"> | 20 | + <ui-badge [tone]="place.priority === 'high' ? 'warm' : 'accent'"> |
| 24 | {{ priorityLabels[place.priority || 'medium'] }} | 21 | {{ priorityLabels[place.priority || 'medium'] }} |
| 25 | - </span> | 22 | + </ui-badge> |
| 26 | </div> | 23 | </div> |
| 27 | <span class="muted">{{ place.country }} · {{ categoryLabels[place.category] }}</span> | 24 | <span class="muted">{{ place.country }} · {{ categoryLabels[place.category] }}</span> |
| 28 | <p class="wish__note" *ngIf="place.note">{{ place.note }}</p> | 25 | <p class="wish__note" *ngIf="place.note">{{ place.note }}</p> |
| @@ -30,7 +27,7 @@ | |||
| 30 | <div class="actions"> | 27 | <div class="actions"> |
| 31 | <button | 28 | <button |
| 32 | type="button" | 29 | type="button" |
| 33 | - class="btn btn--primary btn--sm" | 30 | + ui-button variant="primary" size="sm" |
| 34 | [attr.aria-label]="'Marquer ' + place.name + ' comme visité'" | 31 | [attr.aria-label]="'Marquer ' + place.name + ' comme visité'" |
| 35 | (click)="markVisited(place)" | 32 | (click)="markVisited(place)" |
| 36 | > | 33 | > |
| @@ -40,18 +37,19 @@ | |||
| 40 | <ng-container *ngIf="pendingRemoval === place.id; else removeButton"> | 37 | <ng-container *ngIf="pendingRemoval === place.id; else removeButton"> |
| 41 | <button | 38 | <button |
| 42 | type="button" | 39 | type="button" |
| 43 | - class="btn btn--danger btn--sm" | 40 | + ui-button variant="danger" size="sm" |
| 44 | [attr.aria-label]="'Confirmer la suppression de ' + place.name" | 41 | [attr.aria-label]="'Confirmer la suppression de ' + place.name" |
| 45 | (click)="confirmRemoval(place)" | 42 | (click)="confirmRemoval(place)" |
| 46 | > | 43 | > |
| 47 | Confirmer | 44 | Confirmer |
| 48 | </button> | 45 | </button> |
| 49 | - <button type="button" class="btn btn--sm" (click)="cancelRemoval()">Annuler</button> | 46 | + <button type="button" ui-button size="sm" (click)="cancelRemoval()">Annuler</button> |
| 50 | </ng-container> | 47 | </ng-container> |
| 51 | <ng-template #removeButton> | 48 | <ng-template #removeButton> |
| 52 | <button | 49 | <button |
| 53 | type="button" | 50 | type="button" |
| 54 | - class="btn btn--sm" | 51 | + ui-button |
| 52 | + size="sm" | ||
| 55 | [attr.aria-label]="'Supprimer ' + place.name" | 53 | [attr.aria-label]="'Supprimer ' + place.name" |
| 56 | (click)="askRemoval(place)" | 54 | (click)="askRemoval(place)" |
| 57 | > | 55 | > |
| @@ -63,13 +61,13 @@ | |||
| 63 | </ul> | 61 | </ul> |
| 64 | 62 | ||
| 65 | <ng-template #emptyList> | 63 | <ng-template #emptyList> |
| 66 | - <p class="empty"> | 64 | + <ui-empty-state> |
| 67 | Votre liste est vide. <a routerLink="/lieux/nouveau">Ajoutez une destination</a> qui vous fait rêver. | 65 | Votre liste est vide. <a routerLink="/lieux/nouveau">Ajoutez une destination</a> qui vous fait rêver. |
| 68 | - </p> | 66 | + </ui-empty-state> |
| 69 | </ng-template> | 67 | </ng-template> |
| 70 | - </section> | 68 | + </ui-card> |
| 71 | 69 | ||
| 72 | - <section class="card" aria-labelledby="wish-map-title"> | 70 | + <ui-card aria-labelledby="wish-map-title"> |
| 73 | <h2 id="wish-map-title">Sur la carte</h2> | 71 | <h2 id="wish-map-title">Sur la carte</h2> |
| 74 | <div class="map-slot"> | 72 | <div class="map-slot"> |
| 75 | <app-travel-map | 73 | <app-travel-map |
| @@ -77,6 +75,6 @@ | |||
| 77 | label="Carte des destinations de votre liste de souhaits" | 75 | label="Carte des destinations de votre liste de souhaits" |
| 78 | ></app-travel-map> | 76 | ></app-travel-map> |
| 79 | </div> | 77 | </div> |
| 80 | - </section> | 78 | + </ui-card> |
| 81 | </div> | 79 | </div> |
| 82 | </section> | 80 | </section> |
src/app/travel/pages/wishlist-page/wishlist-page.component.spec.ts +2 -2 | @@ -41,7 +41,7 @@ describe('WishlistPageComponent', () => { | ||
| 41 | 41 | |
| 42 | 42 | it('affiche un message quand la liste est vide', async () => { |
| 43 | 43 | await setup('[]'); |
| 44 | - expect(root.querySelector('.empty')?.textContent).toContain('Votre liste est vide'); | |
| 44 | + expect(root.querySelector('ui-empty-state')?.textContent).toContain('Votre liste est vide'); | |
| 45 | 45 | }); |
| 46 | 46 | |
| 47 | 47 | it('marque un souhait comme visité et prévient l’utilisateur', async () => { |
| @@ -52,7 +52,7 @@ describe('WishlistPageComponent', () => { | ||
| 52 | 52 | |
| 53 | 53 | expect(store.wishlist().some((place) => place.name === 'Kyoto')).toBeFalse(); |
| 54 | 54 | expect(store.visited().some((place) => place.name === 'Kyoto')).toBeTrue(); |
| 55 | - expect(root.querySelector('.notice')?.textContent).toContain('Kyoto rejoint vos lieux visités'); | |
| 55 | + expect(root.querySelector('ui-notice')?.textContent).toContain('Kyoto rejoint vos lieux visités'); | |
| 56 | 56 | }); |
| 57 | 57 | |
| 58 | 58 | it('demande une confirmation avant de supprimer', async () => { |
| @@ -41,7 +41,7 @@ describe('WishlistPageComponent', () => { | |||
| 41 | 41 | ||
| 42 | it('affiche un message quand la liste est vide', async () => { | 42 | it('affiche un message quand la liste est vide', async () => { |
| 43 | await setup('[]'); | 43 | await setup('[]'); |
| 44 | - expect(root.querySelector('.empty')?.textContent).toContain('Votre liste est vide'); | 44 | + expect(root.querySelector('ui-empty-state')?.textContent).toContain('Votre liste est vide'); |
| 45 | }); | 45 | }); |
| 46 | 46 | ||
| 47 | it('marque un souhait comme visité et prévient l’utilisateur', async () => { | 47 | it('marque un souhait comme visité et prévient l’utilisateur', async () => { |
| @@ -52,7 +52,7 @@ describe('WishlistPageComponent', () => { | |||
| 52 | 52 | ||
| 53 | expect(store.wishlist().some((place) => place.name === 'Kyoto')).toBeFalse(); | 53 | expect(store.wishlist().some((place) => place.name === 'Kyoto')).toBeFalse(); |
| 54 | expect(store.visited().some((place) => place.name === 'Kyoto')).toBeTrue(); | 54 | expect(store.visited().some((place) => place.name === 'Kyoto')).toBeTrue(); |
| 55 | - expect(root.querySelector('.notice')?.textContent).toContain('Kyoto rejoint vos lieux visités'); | 55 | + expect(root.querySelector('ui-notice')?.textContent).toContain('Kyoto rejoint vos lieux visités'); |
| 56 | }); | 56 | }); |
| 57 | 57 | ||
| 58 | it('demande une confirmation avant de supprimer', async () => { | 58 | it('demande une confirmation avant de supprimer', async () => { |
src/app/travel/pages/wishlist-page/wishlist-page.component.ts +19 -1 | @@ -1,6 +1,14 @@ | ||
| 1 | 1 | import { CommonModule } from '@angular/common'; |
| 2 | 2 | import { Component } from '@angular/core'; |
| 3 | 3 | import { RouterLink } from '@angular/router'; |
| 4 | +import { | |
| 5 | + UiBadgeComponent, | |
| 6 | + UiButtonComponent, | |
| 7 | + UiCardComponent, | |
| 8 | + UiEmptyStateComponent, | |
| 9 | + UiNoticeComponent, | |
| 10 | + UiPageHeaderComponent, | |
| 11 | +} from 'ui-kit'; | |
| 4 | 12 | import { TravelMapComponent } from '../../components/travel-map/travel-map.component'; |
| 5 | 13 | import { CATEGORY_LABELS, PRIORITY_LABELS, Place } from '../../models/place.model'; |
| 6 | 14 | import { TravelStore } from '../../services/travel-store.service'; |
| @@ -9,7 +17,17 @@ import { todayIso } from '../../utils/date'; | ||
| 9 | 17 | @Component({ |
| 10 | 18 | selector: 'app-wishlist-page', |
| 11 | 19 | standalone: true, |
| 12 | - imports: [CommonModule, RouterLink, TravelMapComponent], | |
| 20 | + imports: [ | |
| 21 | + CommonModule, | |
| 22 | + RouterLink, | |
| 23 | + TravelMapComponent, | |
| 24 | + UiBadgeComponent, | |
| 25 | + UiButtonComponent, | |
| 26 | + UiCardComponent, | |
| 27 | + UiEmptyStateComponent, | |
| 28 | + UiNoticeComponent, | |
| 29 | + UiPageHeaderComponent, | |
| 30 | + ], | |
| 13 | 31 | templateUrl: './wishlist-page.component.html', |
| 14 | 32 | }) |
| 15 | 33 | export class WishlistPageComponent { |
| @@ -1,6 +1,14 @@ | |||
| 1 | import { CommonModule } from '@angular/common'; | 1 | import { CommonModule } from '@angular/common'; |
| 2 | import { Component } from '@angular/core'; | 2 | import { Component } from '@angular/core'; |
| 3 | import { RouterLink } from '@angular/router'; | 3 | import { RouterLink } from '@angular/router'; |
| 4 | +import { | ||
| 5 | + UiBadgeComponent, | ||
| 6 | + UiButtonComponent, | ||
| 7 | + UiCardComponent, | ||
| 8 | + UiEmptyStateComponent, | ||
| 9 | + UiNoticeComponent, | ||
| 10 | + UiPageHeaderComponent, | ||
| 11 | +} from 'ui-kit'; | ||
| 4 | import { TravelMapComponent } from '../../components/travel-map/travel-map.component'; | 12 | import { TravelMapComponent } from '../../components/travel-map/travel-map.component'; |
| 5 | import { CATEGORY_LABELS, PRIORITY_LABELS, Place } from '../../models/place.model'; | 13 | import { CATEGORY_LABELS, PRIORITY_LABELS, Place } from '../../models/place.model'; |
| 6 | import { TravelStore } from '../../services/travel-store.service'; | 14 | import { TravelStore } from '../../services/travel-store.service'; |
| @@ -9,7 +17,17 @@ import { todayIso } from '../../utils/date'; | |||
| 9 | @Component({ | 17 | @Component({ |
| 10 | selector: 'app-wishlist-page', | 18 | selector: 'app-wishlist-page', |
| 11 | standalone: true, | 19 | standalone: true, |
| 12 | - imports: [CommonModule, RouterLink, TravelMapComponent], | 20 | + imports: [ |
| 21 | + CommonModule, | ||
| 22 | + RouterLink, | ||
| 23 | + TravelMapComponent, | ||
| 24 | + UiBadgeComponent, | ||
| 25 | + UiButtonComponent, | ||
| 26 | + UiCardComponent, | ||
| 27 | + UiEmptyStateComponent, | ||
| 28 | + UiNoticeComponent, | ||
| 29 | + UiPageHeaderComponent, | ||
| 30 | + ], | ||
| 13 | templateUrl: './wishlist-page.component.html', | 31 | templateUrl: './wishlist-page.component.html', |
| 14 | }) | 32 | }) |
| 15 | export class WishlistPageComponent { | 33 | export class WishlistPageComponent { |
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.