Skip to content

Commit 4c3ad05

Browse files
committed
add share report by url
1 parent 6f8b524 commit 4c3ad05

7 files changed

Lines changed: 233 additions & 26 deletions

File tree

package-lock.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@angular/platform-browser": "^22.0.1",
2222
"@angular/platform-browser-dynamic": "^22.0.1",
2323
"@angular/router": "^22.0.1",
24+
"lz-string": "^1.5.0",
2425
"rxjs": "~7.8.0",
2526
"three": "^0.165.0",
2627
"tslib": "^2.6.0",
@@ -30,6 +31,7 @@
3031
"@angular-devkit/build-angular": "^22.0.1",
3132
"@angular/cli": "^22.0.1",
3233
"@angular/compiler-cli": "^22.0.1",
34+
"@types/lz-string": "^1.5.0",
3335
"@types/three": "^0.165.0",
3436
"angular-eslint": "^22.0.0",
3537
"eslint": "^9.15.0",

src/app/components/report/report.component.html

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,51 @@
1616
Retour
1717
</button>
1818

19-
<button type="button" class="report__print" (click)="print()">
20-
<svg
21-
width="16"
22-
height="16"
23-
viewBox="0 0 24 24"
24-
fill="none"
25-
stroke="currentColor"
26-
stroke-width="2"
27-
stroke-linecap="round"
28-
stroke-linejoin="round"
19+
<div class="report__actions">
20+
<button
21+
type="button"
22+
class="report__share"
23+
[class.report__share--copied]="copied()"
24+
[disabled]="zones().length === 0"
25+
(click)="copyShareLink()"
2926
>
30-
<polyline points="6 9 6 2 18 2 18 9" />
31-
<path
32-
d="M6 18H4a2 2 0 01-2-2v-5a2 2 0 012-2h16a2 2 0 012 2v5a2 2 0 01-2 2h-2"
33-
/>
34-
<rect x="6" y="14" width="12" height="8" />
35-
</svg>
36-
Imprimer
37-
</button>
27+
@if (copied()) {
28+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
29+
<polyline points="20 6 9 17 4 12" />
30+
</svg>
31+
Lien copié !
32+
} @else {
33+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
34+
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
35+
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
36+
</svg>
37+
Créer un lien de partage
38+
}
39+
</button>
40+
41+
<button type="button" class="report__print" (click)="print()">
42+
<svg
43+
width="16"
44+
height="16"
45+
viewBox="0 0 24 24"
46+
fill="none"
47+
stroke="currentColor"
48+
stroke-width="2"
49+
stroke-linecap="round"
50+
stroke-linejoin="round"
51+
>
52+
<polyline points="6 9 6 2 18 2 18 9" />
53+
<path
54+
d="M6 18H4a2 2 0 01-2-2v-5a2 2 0 012-2h16a2 2 0 012 2v5a2 2 0 01-2 2h-2"
55+
/>
56+
<rect x="6" y="14" width="12" height="8" />
57+
</svg>
58+
Imprimer
59+
</button>
60+
</div>
3861
</header>
3962

63+
4064
<main class="report__body">
4165
<div class="report__logo">
4266
<span class="report__brand-mark">+</span>
@@ -121,8 +145,6 @@ <h1>Doloris - Cartographie de la douleur</h1>
121145
</div>
122146
</div>
123147
}
124-
125-
<p class="footer">Document généré par Doloris</p>
126148
</main>
127149
</div>
128150

src/app/components/report/report.component.scss

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@
1414
background: #f9fbfc;
1515
}
1616

17+
.report__actions {
18+
display: flex;
19+
align-items: center;
20+
gap: 8px;
21+
}
22+
1723
.report__back,
18-
.report__print {
24+
.report__print,
25+
.report__share {
1926
display: flex;
2027
align-items: center;
2128
gap: 6px;
@@ -35,6 +42,61 @@
3542
}
3643
}
3744

45+
.report__share--copied {
46+
color: #2f9e8f;
47+
border-color: #2f9e8f;
48+
}
49+
50+
.report__share:disabled {
51+
opacity: 0.45;
52+
cursor: default;
53+
}
54+
55+
.share-notice {
56+
display: flex;
57+
align-items: center;
58+
justify-content: space-between;
59+
gap: 16px;
60+
padding: 10px 32px;
61+
background: #f0f7f9;
62+
border-bottom: 1px solid #c8dde4;
63+
font-size: 0.875rem;
64+
color: #2e3d45;
65+
66+
&__actions {
67+
display: flex;
68+
gap: 8px;
69+
flex-shrink: 0;
70+
}
71+
72+
&__load,
73+
&__dismiss {
74+
padding: 5px 12px;
75+
border-radius: 5px;
76+
border: 1px solid #dbe5e8;
77+
background: #fff;
78+
font-size: 0.875rem;
79+
font-family: system-ui, sans-serif;
80+
cursor: pointer;
81+
transition: border-color 0.15s;
82+
83+
&:hover {
84+
border-color: #5a7a8a;
85+
}
86+
}
87+
88+
&__load {
89+
background: #2e3d45;
90+
color: #fff;
91+
border-color: #2e3d45;
92+
93+
&:hover {
94+
background: #1a2228;
95+
border-color: #1a2228;
96+
}
97+
}
98+
}
99+
38100
.report__body {
39101
max-width: 720px;
40102
margin: 0 auto;
@@ -242,11 +304,42 @@ hr {
242304
margin: 8px 0 0;
243305
}
244306

307+
.share-footer {
308+
margin-top: 36px;
309+
padding: 14px 16px;
310+
border: 1px solid #dbe5e8;
311+
border-radius: 6px;
312+
background: #f9fbfc;
313+
display: flex;
314+
flex-direction: column;
315+
gap: 6px;
316+
317+
&__label {
318+
font-size: 0.78rem;
319+
color: #8a9da5;
320+
text-transform: uppercase;
321+
letter-spacing: 0.04em;
322+
}
323+
324+
&__url {
325+
font-size: 0.8rem;
326+
color: #2e3d45;
327+
word-break: break-all;
328+
text-decoration: none;
329+
cursor: pointer;
330+
331+
&:hover {
332+
color: #2f9e8f;
333+
text-decoration: underline;
334+
}
335+
}
336+
}
337+
245338
.footer {
246339
font-size: 0.8rem;
247340
color: #8a9da5;
248341
text-align: center;
249-
margin-top: 48px;
342+
margin-top: 24px;
250343
}
251344

252345
.hidden-viewer {

src/app/components/report/report.component.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Component, ChangeDetectionStrategy, inject, signal, effect } from '@angular/core';
1+
import { Component, ChangeDetectionStrategy, inject, signal, computed, effect } from '@angular/core';
22
import { Router } from '@angular/router';
33

44
import { PainDataService } from '../../services/pain-data.service';
5+
import { ShareService } from '../../services/share.service';
56
import { getPainType } from '../../models/pain-types';
67
import { BodyViewerComponent } from '../body-viewer/body-viewer.component';
78

@@ -17,6 +18,7 @@ const COMMENT_KEY = 'pain-mapper:report-comment';
1718
export class ReportComponent {
1819
readonly painData = inject(PainDataService);
1920
private readonly router = inject(Router);
21+
private readonly share = inject(ShareService);
2022

2123
readonly zones = this.painData.zones;
2224
readonly today = new Date().toLocaleDateString('fr-FR', {
@@ -30,7 +32,18 @@ export class ReportComponent {
3032
readonly showViewer = signal(false);
3133
readonly overviewImages = this.painData.overviewImages;
3234

35+
readonly copied = signal(false);
36+
readonly shareUrl = computed(() => this.zones().length > 0 ? this.share.getShareUrl(this.zones()) : null);
37+
3338
constructor() {
39+
const shared = this.share.extractFromFragment();
40+
if (shared) {
41+
this.painData.loadZones(shared);
42+
this.painData.reportImages.set(new Map());
43+
this.painData.overviewImages.set(null);
44+
this.share.clearFragment();
45+
}
46+
3447
const needsZones = this.painData.reportImages().size === 0 && this.painData.zones().length > 0;
3548
const needsOverview = !this.painData.overviewImages();
3649
if (needsZones || needsOverview) {
@@ -78,4 +91,12 @@ export class ReportComponent {
7891
print(): void {
7992
window.print();
8093
}
94+
95+
async copyShareLink(): Promise<void> {
96+
const url = this.share.getShareUrl(this.zones());
97+
await navigator.clipboard.writeText(url);
98+
this.copied.set(true);
99+
setTimeout(() => this.copied.set(false), 2000);
100+
}
101+
81102
}

src/app/services/pain-data.service.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,16 @@ export class PainDataService {
181181
importJson(json: string): void {
182182
const parsed = JSON.parse(json) as { zones: PainZone[] };
183183
if (Array.isArray(parsed.zones)) {
184-
this.zones.set(parsed.zones);
185-
this.selectedZoneId.set(null);
186-
this.redrawTick.update((n) => n + 1);
184+
this.loadZones(parsed.zones);
187185
}
188186
}
189187

188+
loadZones(zones: PainZone[]): void {
189+
this.zones.set(zones);
190+
this.selectedZoneId.set(null);
191+
this.redrawTick.update((n) => n + 1);
192+
}
193+
190194
/** Images capturées depuis le viewer 3D, transmises à la page rapport. */
191195
readonly reportImages = signal<Map<string, string>>(new Map());
192196
}

src/app/services/share.service.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Injectable } from '@angular/core';
2+
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from 'lz-string';
3+
4+
import { PainZone } from '../models/pain-zone.model';
5+
6+
const SHARE_PREFIX = '#share=';
7+
8+
@Injectable({ providedIn: 'root' })
9+
export class ShareService {
10+
private encode(zones: PainZone[]): string {
11+
const slim = zones.map((z) => ({
12+
...z,
13+
points: z.points.map(({ u, v }) => ({ u, v })),
14+
}));
15+
return compressToEncodedURIComponent(JSON.stringify({ zones: slim }));
16+
}
17+
18+
private decode(compressed: string): PainZone[] | null {
19+
try {
20+
const json = decompressFromEncodedURIComponent(compressed);
21+
if (!json) return null;
22+
const parsed = JSON.parse(json) as { zones: PainZone[] };
23+
return Array.isArray(parsed.zones) ? parsed.zones : null;
24+
} catch {
25+
return null;
26+
}
27+
}
28+
29+
getShareUrl(zones: PainZone[]): string {
30+
const encoded = this.encode(zones);
31+
const url = new URL(window.location.href);
32+
url.hash = 'share=' + encoded;
33+
return url.toString();
34+
}
35+
36+
extractFromFragment(): PainZone[] | null {
37+
const hash = window.location.hash;
38+
if (!hash.startsWith(SHARE_PREFIX)) return null;
39+
return this.decode(hash.slice(SHARE_PREFIX.length));
40+
}
41+
42+
clearFragment(): void {
43+
history.replaceState(null, '', window.location.pathname + window.location.search);
44+
}
45+
}

0 commit comments

Comments
 (0)