yatho/demo-angular-migrationpublic Fork
⑂ 2bde847
Commits
⬇ Clone ▾
git clone https://git.rickub.com/yatho/demo-angular-migration.git
git clone ssh://git@rickub.com/yatho/demo-angular-migration.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

feat: Utilisation de la bibliotheque de composantsUnverified

Yann-thomas Le Moigne committed 2026-10-11T13:11:09+02:00 Browse files
2bde847 parent: cbf84e0
modified 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
2525 ## Further help
2626
2727 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 help25 ## 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`.
modified angular.json +4 -2
@@ -35,7 +35,8 @@
3535 "scripts": [],
3636 "allowedCommonJsDependencies": [
3737 "leaflet"
38- ]
38+ ],
39+ "preserveSymlinks": true
3940 },
4041 "configurations": {
4142 "production": {
@@ -93,7 +94,8 @@
9394 "styles": [
9495 "src/styles.css"
9596 ],
96- "scripts": []
97+ "scripts": [],
98+ "preserveSymlinks": true
9799 }
98100 }
99101 }
@@ -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 }
modified package.json +2 -0
@@ -3,6 +3,7 @@
33 "version": "0.0.0",
44 "scripts": {
55 "ng": "ng",
6+ "ui-kit:build": "pnpm --dir ../ui-kit build",
67 "start": "ng serve",
78 "build": "ng build",
89 "watch": "ng build --watch --configuration development",
@@ -21,6 +22,7 @@
2122 "leaflet": "^1.9.4",
2223 "rxjs": "~7.8.0",
2324 "tslib": "^2.3.0",
25+ "ui-kit": "link:../ui-kit/dist/ui-kit",
2426 "zone.js": "~0.14.10"
2527 },
2628 "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": {
modified pnpm-lock.yaml +3 -0
@@ -41,6 +41,9 @@ importers:
4141 tslib:
4242 specifier: ^2.3.0
4343 version: 2.8.1
44+ ui-kit:
45+ specifier: link:../ui-kit/dist/ui-kit
46+ version: link:../ui-kit/dist/ui-kit
4447 zone.js:
4548 specifier: ~0.14.10
4649 version: 0.14.10
@@ -41,6 +41,9 @@ importers:
41 tslib:41 tslib:
42 specifier: ^2.3.042 specifier: ^2.3.0
43 version: 2.8.143 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.1048 specifier: ~0.14.10
46 version: 0.14.1049 version: 0.14.10
modified src/app/app.component.css +1 -1
@@ -48,7 +48,7 @@ nav a:hover {
4848
4949 nav a.is-active {
5050 background: var(--visited);
51- color: var(--on-accent);
51+ color: var(--ui-on-accent);
5252 }
5353
5454 .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 {
deleted 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-}
modified src/app/travel/components/travel-map/travel-map.component.spec.ts +5 -5
@@ -39,9 +39,9 @@ describe('TravelMapComponent', () => {
3939 fixture.componentRef.setInput('places', places);
4040 const root = render();
4141
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);
4545 });
4646
4747 it('met à jour les repères quand la liste change', () => {
@@ -51,12 +51,12 @@ describe('TravelMapComponent', () => {
5151 fixture.componentRef.setInput('places', places.slice(0, 1));
5252 fixture.detectChanges();
5353
54- expect(root.querySelectorAll('.pin').length).toBe(1);
54+ expect(root.querySelectorAll('.ui-map-pin').length).toBe(1);
5555 });
5656
5757 it('affiche le point à placer', () => {
5858 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);
6060 });
6161
6262 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', () => {
modified 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';
153 import { CATEGORY_LABELS, LatLng, Place } from '../../models/place.model';
164 import { formatIsoDate } from '../../utils/date';
175
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. */
207 @Component({
218 selector: 'app-travel-map',
229 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"
15+ [selected]="picked"
16+ [label]="label"
17+ (pointSelected)="locationPicked.emit($event)"
18+ ></ui-map>
19+ `,
2620 styles: `
27- app-travel-map {
21+ :host {
2822 display: block;
2923 height: 100%;
3024 }
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- }
5525 `,
5626 })
57-export class TravelMapComponent implements OnChanges, AfterViewInit, OnDestroy {
27+export class TravelMapComponent implements OnChanges {
5828 @Input() places: readonly Place[] = [];
5929 /** Active la sélection d'un point par clic sur la carte. */
6030 @Input() pickable = false;
@@ -64,108 +34,23 @@ export class TravelMapComponent implements OnChanges, AfterViewInit, OnDestroy {
6434
6535 @Output() locationPicked = new EventEmitter<LatLng>();
6636
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[] = [];
7238
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);
8441 }
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: '&copy; <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- });
15042 }
15143
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 =
16346 place.status === 'visited' && place.visitedOn ? `Visité le ${formatIsoDate(place.visitedOn)}` : 'À visiter';
16447
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,
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+ };
17156 }
@@ -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: '&copy; <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 }
modified src/app/travel/pages/place-form-page/place-form-page.component.html +87 -113
@@ -1,134 +1,108 @@
11 <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>
63
74 <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>
208
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')">
3710 <input
38- id="country"
11+ id="name"
3912 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"
4317 />
44- <p *ngIf="errorOf('country') as message" class="field__error" id="country-error" role="alert">{{ message }}</p>
45- </div>
18+ </ui-field>
4619
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>
5430
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>
6736 </div>
6837
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>
8349
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')">
8851 <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"
9458 />
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>
9762
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>
10481 </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>
11388
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>
12497
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>
130104
131- <section class="card sticky" aria-labelledby="picker-title">
105+ <ui-card class="sticky" aria-labelledby="picker-title">
132106 <h2 id="picker-title">Placer le lieu sur la carte</h2>
133107 <div class="map-slot">
134108 <app-travel-map
@@ -139,6 +113,6 @@
139113 (locationPicked)="onPick($event)"
140114 ></app-travel-map>
141115 </div>
142- </section>
116+ </ui-card>
143117 </div>
144118 </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 <input10 <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- <input34+ </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- <input40+ <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 <input51 <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- <textarea91+ 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-map108 <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>
modified src/app/travel/pages/place-form-page/place-form-page.component.spec.ts +1 -1
@@ -68,7 +68,7 @@ describe('PlaceFormPageComponent', () => {
6868
6969 expect(store.places().length).toBe(0);
7070 expect(navigate).not.toHaveBeenCalled();
71- expect(root.querySelectorAll('.field__error').length).toBeGreaterThanOrEqual(4);
71+ expect(root.querySelectorAll('.ui-field__error').length).toBeGreaterThanOrEqual(4);
7272 });
7373
7474 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', () => {
modified src/app/travel/pages/place-form-page/place-form-page.component.ts +23 -1
@@ -2,6 +2,14 @@ import { CommonModule } from '@angular/common';
22 import { Component, OnInit } from '@angular/core';
33 import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
44 import { Router, RouterLink } from '@angular/router';
5+import {
6+ UiButtonComponent,
7+ UiCardComponent,
8+ UiFieldComponent,
9+ UiOption,
10+ UiPageHeaderComponent,
11+ UiSegmentedComponent,
12+} from 'ui-kit';
513 import { TravelMapComponent } from '../../components/travel-map/travel-map.component';
614 import {
715 CATEGORIES,
@@ -22,7 +30,17 @@ type TextField = 'name' | 'country' | 'lat' | 'lng' | 'visitedOn' | 'note';
2230 @Component({
2331 selector: 'app-place-form-page',
2432 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+ ],
2644 templateUrl: './place-form-page.component.html',
2745 })
2846 export class PlaceFormPageComponent implements OnInit {
@@ -32,6 +50,10 @@ export class PlaceFormPageComponent implements OnInit {
3250 priorityLabels = PRIORITY_LABELS;
3351 ratings = [1, 2, 3, 4, 5];
3452 maxDate = todayIso();
53+ statusOptions: UiOption<PlaceStatus>[] = [
54+ { value: 'visited', label: 'Déjà visité' },
55+ { value: 'wishlist', label: 'Sur ma liste de souhaits' },
56+ ];
3557
3658 status: PlaceStatus = 'visited';
3759 /** 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. */
modified src/app/travel/pages/summary-page/summary-page.component.html +18 -28
@@ -1,43 +1,33 @@
11 <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>
63
74 <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>
117 </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>
1510 </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>
1913 </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>
2316 </li>
2417 </ul>
2518
2619 <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">
2922 <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>
3424 </div>
3525 <div class="map-slot">
3626 <app-travel-map [places]="store.places()" label="Carte de vos lieux visités et de vos souhaits"></app-travel-map>
3727 </div>
38- </section>
28+ </ui-card>
3929
40- <section class="card" aria-labelledby="recent-title">
30+ <ui-card aria-labelledby="recent-title">
4131 <h2 id="recent-title">Derniers voyages</h2>
4232
4333 <ol class="timeline" *ngIf="recent().length > 0; else noTrip">
@@ -48,18 +38,18 @@
4838 </div>
4939 <div class="timeline__meta">
5040 <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>
5242 </div>
5343 <p class="timeline__note" *ngIf="place.note">{{ place.note }}</p>
5444 </li>
5545 </ol>
5646
5747 <ng-template #noTrip>
58- <p class="empty">
48+ <ui-empty-state>
5949 Aucun lieu visité pour l’instant.
6050 <a routerLink="/lieux/nouveau">Ajoutez votre premier voyage</a>.
61- </p>
51+ </ui-empty-state>
6252 </ng-template>
63- </section>
53+ </ui-card>
6454 </div>
6555 </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>
modified src/app/travel/pages/summary-page/summary-page.component.spec.ts +2 -2
@@ -21,7 +21,7 @@ describe('SummaryPageComponent', () => {
2121
2222 it('affiche les statistiques, la carte et les derniers voyages', async () => {
2323 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());
2525
2626 expect(values).toEqual(['5', '5', '4.6', '5']);
2727 expect(root.querySelector('app-travel-map')).not.toBeNull();
@@ -32,7 +32,7 @@ describe('SummaryPageComponent', () => {
3232 it('invite à ajouter un premier voyage quand rien n’est enregistré', async () => {
3333 const root = await render('[]');
3434
35- expect(root.querySelector('.empty a')?.getAttribute('href')).toBe('/lieux/nouveau');
35+ expect(root.querySelector('ui-empty-state a')?.getAttribute('href')).toBe('/lieux/nouveau');
3636 expect(root.querySelector('.timeline')).toBeNull();
3737 });
3838 });
@@ -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 });
modified src/app/travel/pages/summary-page/summary-page.component.ts +24 -2
@@ -1,7 +1,15 @@
11 import { CommonModule } from '@angular/common';
22 import { Component, Signal, computed } from '@angular/core';
33 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';
513 import { TravelMapComponent } from '../../components/travel-map/travel-map.component';
614 import { CATEGORY_LABELS, Place } from '../../models/place.model';
715 import { TravelStore } from '../../services/travel-store.service';
@@ -9,11 +17,25 @@ import { TravelStore } from '../../services/travel-store.service';
917 @Component({
1018 selector: 'app-summary-page',
1119 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+ ],
1331 templateUrl: './summary-page.component.html',
1432 })
1533 export class SummaryPageComponent {
1634 categoryLabels = CATEGORY_LABELS;
35+ legend: UiLegendItem[] = [
36+ { kind: 'primary', label: 'Visité' },
37+ { kind: 'secondary', label: 'À visiter' },
38+ ];
1739 recent: Signal<Place[]>;
1840
1941 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) {
modified src/app/travel/pages/wishlist-page/wishlist-page.component.html +16 -18
@@ -1,17 +1,14 @@
11 <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>
63
7- <p class="notice" role="status" aria-live="polite">
4+ <ui-notice>
85 <ng-container *ngIf="justVisited">
96 {{ justVisited }} rejoint vos lieux visités. <a routerLink="/resume">Voir le résumé</a>
107 </ng-container>
11- </p>
8+ </ui-notice>
129
1310 <div class="layout">
14- <section class="card" aria-labelledby="wishlist-title">
11+ <ui-card aria-labelledby="wishlist-title">
1512 <h2 id="wishlist-title">
1613 {{ store.wishlist().length }} {{ store.wishlist().length > 1 ? 'destinations' : 'destination' }}
1714 </h2>
@@ -20,9 +17,9 @@
2017 <li class="wish" *ngFor="let place of store.wishlist()">
2118 <div class="wish__head">
2219 <strong>{{ place.name }}</strong>
23- <span class="badge" [ngClass]="'badge--' + (place.priority || 'medium')">
20+ <ui-badge [tone]="place.priority === 'high' ? 'warm' : 'accent'">
2421 {{ priorityLabels[place.priority || 'medium'] }}
25- </span>
22+ </ui-badge>
2623 </div>
2724 <span class="muted">{{ place.country }} · {{ categoryLabels[place.category] }}</span>
2825 <p class="wish__note" *ngIf="place.note">{{ place.note }}</p>
@@ -30,7 +27,7 @@
3027 <div class="actions">
3128 <button
3229 type="button"
33- class="btn btn--primary btn--sm"
30+ ui-button variant="primary" size="sm"
3431 [attr.aria-label]="'Marquer ' + place.name + ' comme visité'"
3532 (click)="markVisited(place)"
3633 >
@@ -40,18 +37,19 @@
4037 <ng-container *ngIf="pendingRemoval === place.id; else removeButton">
4138 <button
4239 type="button"
43- class="btn btn--danger btn--sm"
40+ ui-button variant="danger" size="sm"
4441 [attr.aria-label]="'Confirmer la suppression de ' + place.name"
4542 (click)="confirmRemoval(place)"
4643 >
4744 Confirmer
4845 </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>
5047 </ng-container>
5148 <ng-template #removeButton>
5249 <button
5350 type="button"
54- class="btn btn--sm"
51+ ui-button
52+ size="sm"
5553 [attr.aria-label]="'Supprimer ' + place.name"
5654 (click)="askRemoval(place)"
5755 >
@@ -63,13 +61,13 @@
6361 </ul>
6462
6563 <ng-template #emptyList>
66- <p class="empty">
64+ <ui-empty-state>
6765 Votre liste est vide. <a routerLink="/lieux/nouveau">Ajoutez une destination</a> qui vous fait rêver.
68- </p>
66+ </ui-empty-state>
6967 </ng-template>
70- </section>
68+ </ui-card>
7169
72- <section class="card" aria-labelledby="wish-map-title">
70+ <ui-card aria-labelledby="wish-map-title">
7371 <h2 id="wish-map-title">Sur la carte</h2>
7472 <div class="map-slot">
7573 <app-travel-map
@@ -77,6 +75,6 @@
7775 label="Carte des destinations de votre liste de souhaits"
7876 ></app-travel-map>
7977 </div>
80- </section>
78+ </ui-card>
8179 </div>
8280 </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 <button28 <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 <button38 <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 Confirmer44 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 <button49 <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-map73 <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>
modified src/app/travel/pages/wishlist-page/wishlist-page.component.spec.ts +2 -2
@@ -41,7 +41,7 @@ describe('WishlistPageComponent', () => {
4141
4242 it('affiche un message quand la liste est vide', async () => {
4343 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');
4545 });
4646
4747 it('marque un souhait comme visité et prévient l’utilisateur', async () => {
@@ -52,7 +52,7 @@ describe('WishlistPageComponent', () => {
5252
5353 expect(store.wishlist().some((place) => place.name === 'Kyoto')).toBeFalse();
5454 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');
5656 });
5757
5858 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 () => {
modified src/app/travel/pages/wishlist-page/wishlist-page.component.ts +19 -1
@@ -1,6 +1,14 @@
11 import { CommonModule } from '@angular/common';
22 import { Component } from '@angular/core';
33 import { RouterLink } from '@angular/router';
4+import {
5+ UiBadgeComponent,
6+ UiButtonComponent,
7+ UiCardComponent,
8+ UiEmptyStateComponent,
9+ UiNoticeComponent,
10+ UiPageHeaderComponent,
11+} from 'ui-kit';
412 import { TravelMapComponent } from '../../components/travel-map/travel-map.component';
513 import { CATEGORY_LABELS, PRIORITY_LABELS, Place } from '../../models/place.model';
614 import { TravelStore } from '../../services/travel-store.service';
@@ -9,7 +17,17 @@ import { todayIso } from '../../utils/date';
917 @Component({
1018 selector: 'app-wishlist-page',
1119 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+ ],
1331 templateUrl: './wishlist-page.component.html',
1432 })
1533 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 {
modified src/styles.css +14 -229
@@ -1,42 +1,21 @@
11 @import 'leaflet/dist/leaflet.css';
2+@import 'ui-kit/styles/theme.css';
23
4+/* Jetons propres à l'application, alignés sur le thème de la bibliothèque ui-kit. */
35 :root {
4- color-scheme: light dark;
6+ --bg: var(--ui-bg);
7+ --surface: var(--ui-surface);
8+ --ink: var(--ui-ink);
9+ --muted: var(--ui-muted);
10+ --line: var(--ui-line);
11+ --visited: var(--ui-accent);
12+ --visited-soft: var(--ui-accent-soft);
13+ --wish: var(--ui-warm);
14+ --wish-soft: var(--ui-warm-soft);
515
6- --bg: #f5f1ea;
7- --surface: #fffdf9;
8- --ink: #1d2a2f;
9- --muted: #5a686d;
10- --line: #e4ddd0;
11- --visited: #0f766e;
12- --visited-soft: #d9efea;
13- --wish: #b8400c;
14- --wish-soft: #fde6d8;
15- --danger: #b42318;
16- --on-accent: #ffffff;
17- --shadow: 0 1px 2px rgb(29 42 47 / 0.06), 0 10px 28px rgb(29 42 47 / 0.07);
18-
19- --radius: 14px;
2016 --gutter: clamp(1rem, 4vw, 2.5rem);
21- --font-body: 'Avenir Next', 'Segoe UI', system-ui, -apple-system, sans-serif;
22- --font-display: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, serif;
23-}
24-
25-@media (prefers-color-scheme: dark) {
26- :root {
27- --bg: #11181b;
28- --surface: #182226;
29- --ink: #eef2f0;
30- --muted: #a3b3b6;
31- --line: #2b393e;
32- --visited: #46c6b7;
33- --visited-soft: #17332f;
34- --wish: #f5a06c;
35- --wish-soft: #3a2619;
36- --danger: #ff8f84;
37- --on-accent: #0b1416;
38- --shadow: 0 1px 2px rgb(0 0 0 / 0.4);
39- }
17+ --font-body: var(--ui-font-body);
18+ --font-display: var(--ui-font-display);
4019 }
4120
4221 *,
@@ -85,19 +64,6 @@ a {
8564 padding: 1.75rem var(--gutter) 1rem;
8665 }
8766
88-.page__header {
89- margin-bottom: 1.5rem;
90-}
91-
92-.eyebrow {
93- margin: 0 0 0.25rem;
94- color: var(--wish);
95- font-size: 0.8rem;
96- font-weight: 700;
97- letter-spacing: 0.08em;
98- text-transform: uppercase;
99-}
100-
10167 .layout {
10268 display: grid;
10369 gap: 1.25rem;
@@ -125,17 +91,7 @@ a {
12591 margin-top: 0.75rem;
12692 }
12793
128-/* ---------- Cartes et statistiques ---------- */
129-
130-.card {
131- padding: 1.25rem;
132- border: 1px solid var(--line);
133- border-radius: var(--radius);
134- background: var(--surface);
135- box-shadow: var(--shadow);
136-}
137-
138-.card__head {
94+.card-head {
13995 display: flex;
14096 flex-wrap: wrap;
14197 align-items: baseline;
@@ -152,20 +108,6 @@ a {
152108 list-style: none;
153109 }
154110
155-.stat {
156- display: flex;
157- flex-direction: column;
158-}
159-
160-.stat__value {
161- color: var(--visited);
162- font-family: var(--font-display);
163- font-size: 2.4rem;
164- font-weight: 700;
165- line-height: 1;
166-}
167-
168-.stat__label,
169111 .muted,
170112 .hint {
171113 color: var(--muted);
@@ -176,42 +118,6 @@ a {
176118 font-size: 0.85rem;
177119 }
178120
179-.legend {
180- display: flex;
181- gap: 1rem;
182- margin: 0;
183- padding: 0;
184- color: var(--muted);
185- font-size: 0.85rem;
186- list-style: none;
187-}
188-
189-.legend__dot {
190- display: inline-block;
191- width: 0.8rem;
192- height: 0.8rem;
193- margin-right: 0.35rem;
194- border-radius: 50%;
195- vertical-align: -0.1rem;
196-}
197-
198-.legend__dot--visited {
199- background: var(--visited);
200-}
201-
202-.legend__dot--wishlist {
203- border: 2px dashed var(--wish);
204-}
205-
206-.empty,
207-.notice {
208- margin: 1rem 0;
209-}
210-
211-.notice:empty {
212- display: none;
213-}
214-
215121 /* ---------- Listes ---------- */
216122
217123 .timeline,
@@ -250,57 +156,6 @@ a {
250156 margin: 0.2rem 0 0;
251157 }
252158
253-.badge {
254- padding: 0.1rem 0.6rem;
255- border-radius: 999px;
256- background: var(--visited-soft);
257- color: var(--visited);
258- font-size: 0.78rem;
259- font-weight: 700;
260-}
261-
262-.badge--high {
263- background: var(--wish-soft);
264- color: var(--wish);
265-}
266-
267-/* ---------- Boutons ---------- */
268-
269-.btn {
270- display: inline-flex;
271- align-items: center;
272- justify-content: center;
273- padding: 0.65rem 1.1rem;
274- border: 1px solid var(--line);
275- border-radius: 10px;
276- background: var(--surface);
277- color: var(--ink);
278- font: inherit;
279- font-weight: 600;
280- text-decoration: none;
281- cursor: pointer;
282-}
283-
284-.btn:hover {
285- border-color: var(--visited);
286-}
287-
288-.btn--primary {
289- border-color: var(--visited);
290- background: var(--visited);
291- color: var(--on-accent);
292-}
293-
294-.btn--danger {
295- border-color: var(--danger);
296- color: var(--danger);
297-}
298-
299-.btn--sm {
300- padding: 0.4rem 0.8rem;
301- font-size: 0.88rem;
302-}
303-
304159 .actions {
305160 display: flex;
306161 flex-wrap: wrap;
@@ -308,8 +163,6 @@ a {
308163 margin-top: 0.6rem;
309164 }
310165
311-/* ---------- Formulaires ---------- */
312-
313166 .form {
314167 display: grid;
315168 gap: 1.1rem;
@@ -320,71 +173,3 @@ a {
320173 gap: 1rem;
321174 grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
322175 }
323-
324-.field {
325- display: grid;
326- gap: 0.35rem;
327- align-content: start;
328-}
329-
330-.field label,
331-.segmented legend {
332- font-size: 0.9rem;
333- font-weight: 600;
334-}
335-
336-input[type='text'],
337-input[type='number'],
338-input[type='date'],
339-select,
340-textarea {
341- width: 100%;
342- padding: 0.65rem 0.8rem;
343- border: 1px solid var(--line);
344- border-radius: 10px;
345- background: var(--bg);
346- color: var(--ink);
347- font: inherit;
348-}
349-
350-input.ng-invalid.ng-touched,
351-select.ng-invalid.ng-touched,
352-textarea.ng-invalid.ng-touched {
353- border-color: var(--danger);
354-}
355-
356-.field__error {
357- margin: 0;
358- color: var(--danger);
359- font-size: 0.85rem;
360-}
361-
362-.segmented {
363- display: flex;
364- flex-wrap: wrap;
365- gap: 0.5rem;
366- margin: 0;
367- padding: 0;
368- border: 0;
369-}
370-
371-.segmented legend {
372- width: 100%;
373- margin-bottom: 0.35rem;
374- padding: 0;
375-}
376-
377-.segmented label {
378- display: inline-flex;
379- align-items: center;
380- gap: 0.5rem;
381- padding: 0.55rem 0.9rem;
382- border: 1px solid var(--line);
383- border-radius: 999px;
384- cursor: pointer;
385-}
386-
387-.segmented label:has(input:checked) {
388- border-color: var(--visited);
389- background: var(--visited-soft);
390-}
@@ -1,42 +1,21 @@
1 @import 'leaflet/dist/leaflet.css';1 @import 'leaflet/dist/leaflet.css';
2+@import 'ui-kit/styles/theme.css';
2 3
4+/* Jetons propres à l'application, alignés sur le thème de la bibliothèque ui-kit. */
3 :root {5 :root {
4- color-scheme: light dark;6+ --bg: var(--ui-bg);
7+ --surface: var(--ui-surface);
8+ --ink: var(--ui-ink);
9+ --muted: var(--ui-muted);
10+ --line: var(--ui-line);
11+ --visited: var(--ui-accent);
12+ --visited-soft: var(--ui-accent-soft);
13+ --wish: var(--ui-warm);
14+ --wish-soft: var(--ui-warm-soft);
5 15
6- --bg: #f5f1ea;
7- --surface: #fffdf9;
8- --ink: #1d2a2f;
9- --muted: #5a686d;
10- --line: #e4ddd0;
11- --visited: #0f766e;
12- --visited-soft: #d9efea;
13- --wish: #b8400c;
14- --wish-soft: #fde6d8;
15- --danger: #b42318;
16- --on-accent: #ffffff;
17- --shadow: 0 1px 2px rgb(29 42 47 / 0.06), 0 10px 28px rgb(29 42 47 / 0.07);
18-
19- --radius: 14px;
20 --gutter: clamp(1rem, 4vw, 2.5rem);16 --gutter: clamp(1rem, 4vw, 2.5rem);
21- --font-body: 'Avenir Next', 'Segoe UI', system-ui, -apple-system, sans-serif;17+ --font-body: var(--ui-font-body);
22- --font-display: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, serif;18+ --font-display: var(--ui-font-display);
23-}
24-
25-@media (prefers-color-scheme: dark) {
26- :root {
27- --bg: #11181b;
28- --surface: #182226;
29- --ink: #eef2f0;
30- --muted: #a3b3b6;
31- --line: #2b393e;
32- --visited: #46c6b7;
33- --visited-soft: #17332f;
34- --wish: #f5a06c;
35- --wish-soft: #3a2619;
36- --danger: #ff8f84;
37- --on-accent: #0b1416;
38- --shadow: 0 1px 2px rgb(0 0 0 / 0.4);
39- }
40 }19 }
41 20
42 *,21 *,
@@ -85,19 +64,6 @@ a {
85 padding: 1.75rem var(--gutter) 1rem;64 padding: 1.75rem var(--gutter) 1rem;
86 }65 }
87 66
88-.page__header {
89- margin-bottom: 1.5rem;
90-}
91-
92-.eyebrow {
93- margin: 0 0 0.25rem;
94- color: var(--wish);
95- font-size: 0.8rem;
96- font-weight: 700;
97- letter-spacing: 0.08em;
98- text-transform: uppercase;
99-}
100-
101 .layout {67 .layout {
102 display: grid;68 display: grid;
103 gap: 1.25rem;69 gap: 1.25rem;
@@ -125,17 +91,7 @@ a {
125 margin-top: 0.75rem;91 margin-top: 0.75rem;
126 }92 }
127 93
128-/* ---------- Cartes et statistiques ---------- */94+.card-head {
129-
130-.card {
131- padding: 1.25rem;
132- border: 1px solid var(--line);
133- border-radius: var(--radius);
134- background: var(--surface);
135- box-shadow: var(--shadow);
136-}
137-
138-.card__head {
139 display: flex;95 display: flex;
140 flex-wrap: wrap;96 flex-wrap: wrap;
141 align-items: baseline;97 align-items: baseline;
@@ -152,20 +108,6 @@ a {
152 list-style: none;108 list-style: none;
153 }109 }
154 110
155-.stat {
156- display: flex;
157- flex-direction: column;
158-}
159-
160-.stat__value {
161- color: var(--visited);
162- font-family: var(--font-display);
163- font-size: 2.4rem;
164- font-weight: 700;
165- line-height: 1;
166-}
167-
168-.stat__label,
169 .muted,111 .muted,
170 .hint {112 .hint {
171 color: var(--muted);113 color: var(--muted);
@@ -176,42 +118,6 @@ a {
176 font-size: 0.85rem;118 font-size: 0.85rem;
177 }119 }
178 120
179-.legend {
180- display: flex;
181- gap: 1rem;
182- margin: 0;
183- padding: 0;
184- color: var(--muted);
185- font-size: 0.85rem;
186- list-style: none;
187-}
188-
189-.legend__dot {
190- display: inline-block;
191- width: 0.8rem;
192- height: 0.8rem;
193- margin-right: 0.35rem;
194- border-radius: 50%;
195- vertical-align: -0.1rem;
196-}
197-
198-.legend__dot--visited {
199- background: var(--visited);
200-}
201-
202-.legend__dot--wishlist {
203- border: 2px dashed var(--wish);
204-}
205-
206-.empty,
207-.notice {
208- margin: 1rem 0;
209-}
210-
211-.notice:empty {
212- display: none;
213-}
214-
215 /* ---------- Listes ---------- */121 /* ---------- Listes ---------- */
216 122
217 .timeline,123 .timeline,
@@ -250,57 +156,6 @@ a {
250 margin: 0.2rem 0 0;156 margin: 0.2rem 0 0;
251 }157 }
252 158
253-.badge {
254- padding: 0.1rem 0.6rem;
255- border-radius: 999px;
256- background: var(--visited-soft);
257- color: var(--visited);
258- font-size: 0.78rem;
259- font-weight: 700;
260-}
261-
262-.badge--high {
263- background: var(--wish-soft);
264- color: var(--wish);
265-}
266-
267-/* ---------- Boutons ---------- */
268-
269-.btn {
270- display: inline-flex;
271- align-items: center;
272- justify-content: center;
273- padding: 0.65rem 1.1rem;
274- border: 1px solid var(--line);
275- border-radius: 10px;
276- background: var(--surface);
277- color: var(--ink);
278- font: inherit;
279- font-weight: 600;
280- text-decoration: none;
281- cursor: pointer;
282-}
283-
284-.btn:hover {
285- border-color: var(--visited);
286-}
287-
288-.btn--primary {
289- border-color: var(--visited);
290- background: var(--visited);
291- color: var(--on-accent);
292-}
293-
294-.btn--danger {
295- border-color: var(--danger);
296- color: var(--danger);
297-}
298-
299-.btn--sm {
300- padding: 0.4rem 0.8rem;
301- font-size: 0.88rem;
302-}
303-
304 .actions {159 .actions {
305 display: flex;160 display: flex;
306 flex-wrap: wrap;161 flex-wrap: wrap;
@@ -308,8 +163,6 @@ a {
308 margin-top: 0.6rem;163 margin-top: 0.6rem;
309 }164 }
310 165
311-/* ---------- Formulaires ---------- */
312-
313 .form {166 .form {
314 display: grid;167 display: grid;
315 gap: 1.1rem;168 gap: 1.1rem;
@@ -320,71 +173,3 @@ a {
320 gap: 1rem;173 gap: 1rem;
321 grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));174 grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
322 }175 }
323-
324-.field {
325- display: grid;
326- gap: 0.35rem;
327- align-content: start;
328-}
329-
330-.field label,
331-.segmented legend {
332- font-size: 0.9rem;
333- font-weight: 600;
334-}
335-
336-input[type='text'],
337-input[type='number'],
338-input[type='date'],
339-select,
340-textarea {
341- width: 100%;
342- padding: 0.65rem 0.8rem;
343- border: 1px solid var(--line);
344- border-radius: 10px;
345- background: var(--bg);
346- color: var(--ink);
347- font: inherit;
348-}
349-
350-input.ng-invalid.ng-touched,
351-select.ng-invalid.ng-touched,
352-textarea.ng-invalid.ng-touched {
353- border-color: var(--danger);
354-}
355-
356-.field__error {
357- margin: 0;
358- color: var(--danger);
359- font-size: 0.85rem;
360-}
361-
362-.segmented {
363- display: flex;
364- flex-wrap: wrap;
365- gap: 0.5rem;
366- margin: 0;
367- padding: 0;
368- border: 0;
369-}
370-
371-.segmented legend {
372- width: 100%;
373- margin-bottom: 0.35rem;
374- padding: 0;
375-}
376-
377-.segmented label {
378- display: inline-flex;
379- align-items: center;
380- gap: 0.5rem;
381- padding: 0.55rem 0.9rem;
382- border: 1px solid var(--line);
383- border-radius: 999px;
384- cursor: pointer;
385-}
386-
387-.segmented label:has(input:checked) {
388- border-color: var(--visited);
389- background: var(--visited-soft);
390-}