@testgorilla/tgo-immersive-test 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.eslintrc.json +46 -0
  2. package/README.md +89 -0
  3. package/jest.config.ts +28 -0
  4. package/ng-package.json +16 -0
  5. package/package.json +25 -0
  6. package/project.json +37 -0
  7. package/src/assets/i18n/en.json +18 -0
  8. package/src/index.ts +4 -0
  9. package/src/lib/components/immersive-test/immersive-test.component.html +100 -0
  10. package/src/lib/components/immersive-test/immersive-test.component.scss +247 -0
  11. package/src/lib/components/immersive-test/immersive-test.component.spec.ts +581 -0
  12. package/src/lib/components/immersive-test/immersive-test.component.ts +279 -0
  13. package/src/lib/components/index.ts +6 -0
  14. package/src/lib/components/review-instructions-dialog/index.ts +1 -0
  15. package/src/lib/components/review-instructions-dialog/review-instructions-dialog.component.html +39 -0
  16. package/src/lib/components/review-instructions-dialog/review-instructions-dialog.component.scss +160 -0
  17. package/src/lib/components/review-instructions-dialog/review-instructions-dialog.component.spec.ts +81 -0
  18. package/src/lib/components/review-instructions-dialog/review-instructions-dialog.component.ts +80 -0
  19. package/src/lib/components/ringing-phone-animation/index.ts +1 -0
  20. package/src/lib/components/ringing-phone-animation/ringing-phone-animation.component.html +16 -0
  21. package/src/lib/components/ringing-phone-animation/ringing-phone-animation.component.scss +79 -0
  22. package/src/lib/components/ringing-phone-animation/ringing-phone-animation.component.spec.ts +95 -0
  23. package/src/lib/components/ringing-phone-animation/ringing-phone-animation.component.ts +54 -0
  24. package/src/lib/components/ringing-phone-animation/ringing-phone-animation.sound.ts +2 -0
  25. package/src/lib/components/video-countdown/index.ts +1 -0
  26. package/src/lib/components/video-countdown/video-countdown.component.html +10 -0
  27. package/src/lib/components/video-countdown/video-countdown.component.scss +16 -0
  28. package/src/lib/components/video-countdown/video-countdown.component.spec.ts +59 -0
  29. package/src/lib/components/video-countdown/video-countdown.component.ts +102 -0
  30. package/src/lib/models/index.ts +9 -0
  31. package/src/lib/models/translations.ts +3 -0
  32. package/src/lib/services/index.ts +7 -0
  33. package/src/test-setup.ts +22 -0
  34. package/tsconfig.json +17 -0
  35. package/tsconfig.lib.json +15 -0
  36. package/tsconfig.lib.prod.json +10 -0
  37. package/tsconfig.spec.json +13 -0
@@ -0,0 +1,279 @@
1
+ import {
2
+ AudioAnimationComponent,
3
+ VimeoVideoComponent,
4
+ Question,
5
+ TestResultRead,
6
+ SelectedMediaDevices,
7
+ ISubmissionState,
8
+ IQuestionDataContract,
9
+ MediaService,
10
+ ThemeService,
11
+ TranslocoLazyModuleUtils,
12
+ getAvailableLangs,
13
+ } from '@testgorilla/tgo-test-shared';
14
+ import { VideoCountdownComponent } from '../video-countdown';
15
+ import { RingingPhoneAnimationComponent } from '../ringing-phone-animation';
16
+ import {
17
+ ReviewInstructionsDialogComponent,
18
+ ReviewInstructionsDialogData,
19
+ } from '../review-instructions-dialog';
20
+ import { ROOT_TRANSLATIONS_SCOPE } from '../../models/translations';
21
+ import { catchError, firstValueFrom, Observable, takeUntil } from 'rxjs';
22
+ import { transition, style, animate, trigger } from '@angular/animations';
23
+ import {
24
+ ElementRef,
25
+ OnDestroy,
26
+ signal,
27
+ ViewChild,
28
+ Component,
29
+ EventEmitter,
30
+ Input,
31
+ Output,
32
+ inject,
33
+ OnInit,
34
+ Inject,
35
+ ChangeDetectorRef,
36
+ } from '@angular/core';
37
+ import { Subject } from 'rxjs';
38
+ import {
39
+ ButtonComponentModule,
40
+ DialogService,
41
+ IconComponentModule,
42
+ } from '@testgorilla/tgo-ui';
43
+ import { CommonModule } from '@angular/common';
44
+ import {
45
+ TRANSLOCO_SCOPE,
46
+ TranslocoModule,
47
+ TranslocoScope,
48
+ TranslocoService,
49
+ } from '@ngneat/transloco';
50
+
51
+ @Component({
52
+ selector: 'tgo-immersive-test',
53
+ templateUrl: './immersive-test.component.html',
54
+ styleUrl: './immersive-test.component.scss',
55
+ animations: [
56
+ trigger('fadeInFadeOut', [
57
+ transition(':enter', [
58
+ style({ opacity: 0 }),
59
+ animate('600ms', style({ opacity: 1 })),
60
+ ]),
61
+ transition(':leave', [animate('600ms', style({ opacity: 0 }))]),
62
+ ]),
63
+ ],
64
+ standalone: true,
65
+ imports: [
66
+ TranslocoModule,
67
+ ButtonComponentModule,
68
+ IconComponentModule,
69
+ CommonModule,
70
+ VimeoVideoComponent,
71
+ VideoCountdownComponent,
72
+ RingingPhoneAnimationComponent,
73
+ AudioAnimationComponent,
74
+ ReviewInstructionsDialogComponent,
75
+ ],
76
+ providers: [
77
+ TranslocoLazyModuleUtils.getScopeProvider(
78
+ 'tgo-immersive-test',
79
+ getAvailableLangs(),
80
+ ROOT_TRANSLATIONS_SCOPE,
81
+ (lang: string) => import(`../../../assets/i18n/${lang}.json`)
82
+ ),
83
+ DialogService,
84
+ ThemeService,
85
+ ],
86
+ })
87
+ export class ImmersiveTestComponent
88
+ implements OnInit, OnDestroy, IQuestionDataContract
89
+ {
90
+ @ViewChild('video') videoElement?: ElementRef<HTMLVideoElement>;
91
+ @ViewChild('audio') audioElement?: ElementRef<HTMLAudioElement>;
92
+ @Input({ required: true }) question!: Question;
93
+ @Input({ required: true }) test!: TestResultRead;
94
+ @Input() isFirstQuestion?: boolean;
95
+ @Input() expirationObservable?: Observable<void>;
96
+ @Input() selectedMediaDevices?: SelectedMediaDevices;
97
+ @Input() mediaAccessChanged?: Observable<SelectedMediaDevices> | undefined;
98
+
99
+ @Output() submissionStateChanged =
100
+ new EventEmitter<ISubmissionState | null>();
101
+ @Output() loadingStateChanged: EventEmitter<boolean> =
102
+ new EventEmitter<boolean>();
103
+ @Output() requestMediaAccess: EventEmitter<void> = new EventEmitter<void>();
104
+
105
+ isAnswering = signal(false);
106
+ isCountingDown = signal(false);
107
+ isVideoPlaying = signal(false);
108
+ isQuestionPlaying = signal(false);
109
+ candidateVideoStreamReady = signal(false);
110
+ translations: { [key: string]: string } = {};
111
+ volume = signal(0);
112
+ audioUrl = signal('');
113
+
114
+ private unsubscribe$ = new Subject<void>();
115
+ private mediaService = inject(MediaService);
116
+ private translocoService = inject(TranslocoService);
117
+ private dialog = inject(DialogService);
118
+ private cdr = inject(ChangeDetectorRef);
119
+ private themeService = inject(ThemeService);
120
+
121
+ companyColor = this.themeService.getCompanyColor();
122
+
123
+ get fileUrl(): string {
124
+ const match = this.question?.text.match(/(src|href)="([^"]*)"/);
125
+ return (match && match.pop()) || '';
126
+ }
127
+
128
+ get isVideo() {
129
+ return this.fileUrl.includes('player.vimeo.com');
130
+ }
131
+
132
+ constructor(
133
+ @Inject(TRANSLOCO_SCOPE) private translationScope: TranslocoScope
134
+ ) {}
135
+
136
+ ngOnInit(): void {
137
+ this.initExpirationSubscription();
138
+ this.initMediaAccessSubscription();
139
+ this.loadingStateChanged.emit(false);
140
+ this.mediaService.setSelectedMediaDevices(this.selectedMediaDevices);
141
+ void this.setTranslations();
142
+ void this.initVideoStream();
143
+ if (!this.isFirstQuestion) {
144
+ void this.startQuestion();
145
+ }
146
+ }
147
+
148
+ ngOnDestroy() {
149
+ this.unsubscribe$.next();
150
+ this.unsubscribe$.complete();
151
+ }
152
+
153
+ stopRecording() {
154
+ this.mediaService.stopRecording();
155
+ }
156
+
157
+ onVideoLoad() {
158
+ this.candidateVideoStreamReady.set(true);
159
+ }
160
+
161
+ async startQuestion() {
162
+ this.isQuestionPlaying.set(true);
163
+
164
+ if (!this.isVideo) {
165
+ await this.mediaService.playAudio(this.fileUrl);
166
+ this.startCountdown();
167
+ }
168
+ }
169
+
170
+ async startCountdown() {
171
+ if (
172
+ !(await this.mediaService.checkPermission({ audio: true, video: false }))
173
+ ) {
174
+ this.requestMediaAccess.emit();
175
+ return;
176
+ }
177
+ this.isCountingDown.set(true);
178
+ this.isVideoPlaying.set(false);
179
+ }
180
+
181
+ startRecordAnswer() {
182
+ this.isCountingDown.set(false);
183
+ this.isAnswering.set(true);
184
+ this.mediaService
185
+ .recordAudio()
186
+ .pipe(
187
+ takeUntil(this.unsubscribe$),
188
+ catchError((error) => {
189
+ console.error('Error recording answer', error);
190
+ this.isAnswering.set(false);
191
+ this.startCountdown();
192
+ throw error;
193
+ })
194
+ )
195
+ .subscribe((event) => {
196
+ if (event.type === 'complete') {
197
+ if (!this.test.is_preview_mode) {
198
+ this.submissionStateChanged.emit({ file: event.file, text: '' });
199
+ this.isAnswering.set(false);
200
+ this.loadingStateChanged.emit(true);
201
+ } else {
202
+ const url = window.URL.createObjectURL(event.file);
203
+ this.audioUrl.set(url);
204
+ if (this.audioElement) {
205
+ this.audioElement.nativeElement.src = url;
206
+ this.audioElement.nativeElement.play();
207
+ }
208
+ }
209
+ } else {
210
+ this.volume.set(event.value);
211
+ }
212
+ });
213
+ }
214
+
215
+ openReviewInstructionsDialog() {
216
+ const dialogData: ReviewInstructionsDialogData = {
217
+ backgroundInfoData: this.test.intro_text,
218
+ instructionsInfoData: this.test.test_instruction,
219
+ };
220
+ this.dialog.open(ReviewInstructionsDialogComponent, {
221
+ size: 'large',
222
+ extraData: dialogData,
223
+ });
224
+ }
225
+
226
+ private async initVideoStream() {
227
+ try {
228
+ const stream = await this.mediaService.getMediaStream({
229
+ video: true,
230
+ audio: false,
231
+ });
232
+ if (this.videoElement) {
233
+ this.videoElement.nativeElement.srcObject = stream;
234
+ await this.videoElement?.nativeElement.play();
235
+ }
236
+ } catch (error) {
237
+ console.error('Error initializing video stream:', error);
238
+ this.candidateVideoStreamReady.set(false);
239
+ }
240
+ }
241
+
242
+ private async setTranslations() {
243
+ this.translations = await firstValueFrom(
244
+ this.translocoService.selectTranslateObject(
245
+ `TEST`,
246
+ {},
247
+ this.translationScope as string
248
+ )
249
+ );
250
+ this.cdr.markForCheck();
251
+ }
252
+
253
+ private initExpirationSubscription() {
254
+ this.expirationObservable
255
+ ?.pipe(takeUntil(this.unsubscribe$))
256
+ .subscribe(() => {
257
+ if (!this.test?.is_preview_mode) {
258
+ if (this.mediaService.isRecording()) {
259
+ this.stopRecording();
260
+ } else {
261
+ this.submissionStateChanged.emit({ file: undefined, text: '' });
262
+ }
263
+ }
264
+ });
265
+ }
266
+
267
+ private initMediaAccessSubscription() {
268
+ this.mediaAccessChanged
269
+ ?.pipe(takeUntil(this.unsubscribe$))
270
+ .subscribe((selectedMediaDevices) => {
271
+ this.mediaService.setSelectedMediaDevices(selectedMediaDevices);
272
+ if (!this.isCountingDown()) {
273
+ void this.startCountdown();
274
+ }
275
+ void this.initVideoStream();
276
+ });
277
+ }
278
+ }
279
+
@@ -0,0 +1,6 @@
1
+ // Library-specific components only (shared components are in @testgorilla/tgo-test-shared)
2
+ export * from './immersive-test/immersive-test.component';
3
+ export * from './review-instructions-dialog';
4
+ export * from './ringing-phone-animation';
5
+ export * from './video-countdown';
6
+
@@ -0,0 +1 @@
1
+ export * from './review-instructions-dialog.component';
@@ -0,0 +1,39 @@
1
+ <ng-container *transloco="let t">
2
+ <ui-dialog
3
+ class="dialog-wrapper"
4
+ [title]="translations['TITLE']"
5
+ [secondaryButtonLabel]="translations['CLOSE']"
6
+ (secondaryButtonClickEvent)="closeDialog()"
7
+ >
8
+ <section class="containers-section">
9
+ <div class="immersive-test-instructions-container">
10
+ <div
11
+ class="background-information-container"
12
+ *ngIf="dialogData?.backgroundInfoData"
13
+ >
14
+ <ui-card class="background-information-card" variant="neutral">
15
+ <quill-view
16
+ theme="snow"
17
+ [content]="dialogData?.backgroundInfoData"
18
+ class="content-container notranslate"
19
+ ></quill-view>
20
+ </ui-card>
21
+ </div>
22
+
23
+ <div
24
+ class="inner-instructions-container"
25
+ *ngIf="dialogData?.instructionsInfoData"
26
+ >
27
+ <ui-card class="instructions-card" variant="educative">
28
+ <quill-view
29
+ theme="snow"
30
+ [content]="dialogData?.instructionsInfoData"
31
+ class="content-container notranslate"
32
+ ></quill-view>
33
+ </ui-card>
34
+ </div>
35
+ </div>
36
+ </section>
37
+ </ui-dialog>
38
+ </ng-container>
39
+
@@ -0,0 +1,160 @@
1
+ :host {
2
+ .dialog-wrapper {
3
+ width: 100%;
4
+
5
+ ::ng-deep {
6
+ .mat-mdc-dialog-content {
7
+ .containers-section {
8
+ width: 100%;
9
+ display: flex;
10
+ flex-direction: column;
11
+ justify-content: center;
12
+ padding: 0.3rem 0.8rem;
13
+
14
+ .immersive-test-instructions-container {
15
+ display: flex;
16
+ justify-content: space-between;
17
+ width: 100%;
18
+ gap: 2.5rem;
19
+
20
+ .background-information-container {
21
+ display: flex;
22
+ flex-direction: column;
23
+ width: 100%;
24
+ max-width: 450px;
25
+
26
+ .background-information-card {
27
+ .content-container {
28
+ .ql-container {
29
+ .ql-editor {
30
+ padding: 0;
31
+
32
+ h1,
33
+ h2,
34
+ h3,
35
+ h4,
36
+ h5,
37
+ h6 {
38
+ font-size: 16px;
39
+ font-weight: 700;
40
+ line-height: 20px;
41
+ margin-bottom: 0.6rem;
42
+ }
43
+
44
+ p,
45
+ span {
46
+ font-size: 14px;
47
+ margin-bottom: 1.5rem;
48
+ }
49
+
50
+ p:last-child,
51
+ span:last-child {
52
+ margin-bottom: 0;
53
+ }
54
+
55
+ br {
56
+ display: none;
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ .inner-instructions-container {
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: 2.5rem;
68
+ width: 100%;
69
+ max-width: 450px;
70
+
71
+ .instructions-card {
72
+ .card-container {
73
+ border-width: 1px;
74
+
75
+ .ql-container {
76
+ .ql-editor {
77
+ padding: 0;
78
+
79
+ h1,
80
+ h2,
81
+ h3,
82
+ h4,
83
+ h5,
84
+ h6 {
85
+ font-size: 16px;
86
+ font-weight: 700;
87
+ line-height: 20px;
88
+ margin-bottom: 0.6rem;
89
+ }
90
+
91
+ p::before,
92
+ span::before {
93
+ content: '• ';
94
+ color: black;
95
+ font-size: 1em;
96
+ position: absolute;
97
+ left: 8px;
98
+ top: 0;
99
+ }
100
+
101
+ p,
102
+ span {
103
+ position: relative;
104
+ font-size: 14px;
105
+ padding-left: 22px;
106
+ margin: 3px 0 0;
107
+ }
108
+
109
+ ul,
110
+ ol {
111
+ padding-left: 10px;
112
+ margin: 5px 0 0;
113
+
114
+ li {
115
+ font-size: 14px;
116
+ padding-left: 0;
117
+
118
+ span::before {
119
+ top: -3px;
120
+ }
121
+
122
+ span {
123
+ padding-left: 12px;
124
+ }
125
+ }
126
+ }
127
+
128
+ br {
129
+ display: none;
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ @media (max-width: 799px) {
140
+ .containers-section {
141
+ padding-top: 0;
142
+
143
+ .immersive-test-instructions-container {
144
+ flex-wrap: wrap;
145
+
146
+ .background-information-container {
147
+ max-width: none;
148
+ }
149
+
150
+ .inner-instructions-container {
151
+ max-width: none;
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+
@@ -0,0 +1,81 @@
1
+ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
2
+ import { ReviewInstructionsDialogComponent } from './review-instructions-dialog.component';
3
+ import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core';
4
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
5
+ import { TRANSLOCO_SCOPE, TranslocoModule, TranslocoService } from '@ngneat/transloco';
6
+ import { of } from 'rxjs';
7
+
8
+ const mockedTranslations = { testTranslationKey: 'test-translation' };
9
+ const mockDialog = { close: jest.fn() };
10
+
11
+ describe('ReviewInstructionsDialogComponent', () => {
12
+ let component: ReviewInstructionsDialogComponent;
13
+ let fixture: ComponentFixture<ReviewInstructionsDialogComponent>;
14
+ let transLocoServiceMock: Partial<jest.Mocked<TranslocoService>>;
15
+
16
+ beforeEach(async () => {
17
+ transLocoServiceMock = {
18
+ selectTranslateObject: jest.fn().mockReturnValue(of(mockedTranslations)),
19
+ };
20
+ await TestBed.configureTestingModule({
21
+ imports: [ReviewInstructionsDialogComponent, TranslocoModule],
22
+ schemas: [NO_ERRORS_SCHEMA],
23
+ providers: [
24
+ { provide: TranslocoService, useValue: transLocoServiceMock },
25
+ {
26
+ provide: TRANSLOCO_SCOPE,
27
+ useValue: 'tgo-immersive-test-review-instructions',
28
+ },
29
+ {
30
+ provide: ChangeDetectorRef,
31
+ useValue: { markForCheck: jest.fn() },
32
+ },
33
+ {
34
+ provide: MAT_DIALOG_DATA,
35
+ useValue: {
36
+ backgroundInfoData: 'backgroundInfoData',
37
+ instructionsInfoData: 'instructionsInfoData',
38
+ },
39
+ },
40
+ {
41
+ provide: MatDialogRef,
42
+ useValue: mockDialog,
43
+ },
44
+ ],
45
+ })
46
+ .overrideComponent(ReviewInstructionsDialogComponent, {
47
+ set: {
48
+ providers: [],
49
+ },
50
+ })
51
+ .compileComponents();
52
+ fixture = TestBed.createComponent(ReviewInstructionsDialogComponent);
53
+ component = fixture.componentInstance;
54
+ });
55
+
56
+ afterEach(() => {
57
+ jest.clearAllMocks();
58
+ fixture.destroy();
59
+ });
60
+
61
+ describe('when initialized', () => {
62
+ beforeEach(() => {
63
+ component.ngOnInit();
64
+ });
65
+
66
+ it('should set translations', () => {
67
+ expect(component.translations).toEqual(mockedTranslations);
68
+ });
69
+
70
+ describe('when call closeDialog', () => {
71
+ beforeEach(() => {
72
+ component.closeDialog();
73
+ });
74
+
75
+ it('should call dialog close', () => {
76
+ expect(mockDialog.close).toHaveBeenCalledWith(false);
77
+ });
78
+ });
79
+ });
80
+ });
81
+
@@ -0,0 +1,80 @@
1
+ import { CommonModule } from '@angular/common';
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ ChangeDetectorRef,
5
+ Component,
6
+ inject,
7
+ Inject,
8
+ OnInit,
9
+ } from '@angular/core';
10
+ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
11
+ import {
12
+ TRANSLOCO_SCOPE,
13
+ TranslocoModule,
14
+ TranslocoScope,
15
+ TranslocoService,
16
+ } from '@ngneat/transloco';
17
+ import { CardComponentModule, DialogComponentModule } from '@testgorilla/tgo-ui';
18
+ import { QuillViewComponent } from 'ngx-quill';
19
+ import { firstValueFrom } from 'rxjs';
20
+ import { getAvailableLangs, TranslocoLazyModuleUtils } from '../../services';
21
+
22
+ export interface ReviewInstructionsDialogData {
23
+ backgroundInfoData?: string;
24
+ instructionsInfoData?: string;
25
+ }
26
+
27
+ @Component({
28
+ selector: 'tgo-review-instructions-dialog',
29
+ templateUrl: './review-instructions-dialog.component.html',
30
+ styleUrls: ['./review-instructions-dialog.component.scss'],
31
+ providers: [
32
+ TranslocoLazyModuleUtils.getScopeProvider(
33
+ 'tgo-immersive-test-review-instructions',
34
+ getAvailableLangs(),
35
+ 'INSTRUCTIONS_MODAL',
36
+ (lang: string) => import(`../../../assets/i18n/${lang}.json`)
37
+ ),
38
+ ],
39
+ changeDetection: ChangeDetectionStrategy.OnPush,
40
+ standalone: true,
41
+ imports: [
42
+ CommonModule,
43
+ TranslocoModule,
44
+ DialogComponentModule,
45
+ CardComponentModule,
46
+ QuillViewComponent,
47
+ ],
48
+ })
49
+ export class ReviewInstructionsDialogComponent implements OnInit {
50
+ translations: { [key: string]: string } = {};
51
+
52
+ private translocoService = inject(TranslocoService);
53
+ private cdr = inject(ChangeDetectorRef);
54
+
55
+ constructor(
56
+ @Inject(MAT_DIALOG_DATA)
57
+ public dialogData: ReviewInstructionsDialogData,
58
+ @Inject(TRANSLOCO_SCOPE) private translationScope: TranslocoScope,
59
+ private dialogRef: MatDialogRef<ReviewInstructionsDialogComponent>
60
+ ) {}
61
+
62
+ ngOnInit(): void {
63
+ void this.setTranslations();
64
+ }
65
+
66
+ closeDialog(): void {
67
+ this.dialogRef.close(false);
68
+ }
69
+
70
+ private async setTranslations(): Promise<void> {
71
+ this.translations = await firstValueFrom(
72
+ this.translocoService.selectTranslateObject(
73
+ `INSTRUCTIONS_MODAL`,
74
+ {},
75
+ this.translationScope as string
76
+ )
77
+ );
78
+ this.cdr.markForCheck();
79
+ }
80
+ }
@@ -0,0 +1 @@
1
+ export * from './ringing-phone-animation.component';
@@ -0,0 +1,16 @@
1
+ <div class="ringing-phone-animation-wrapper">
2
+ <section class="ringing-phone-animation-container">
3
+ <div class="ringing-effect" *ngIf="ringingSignal()">
4
+ <div class="ringing-line first-line"></div>
5
+ <div class="ringing-line second-line"></div>
6
+ <div class="ringing-line third-line"></div>
7
+ </div>
8
+ <svg width="76" height="30" viewBox="0 0 76 30" fill="none" xmlns="http://www.w3.org/2000/svg">
9
+ <path
10
+ d="M9.33348 28.5122L1.66681 21.0122C1.00014 20.3455 0.666809 19.5678 0.666809 18.6789C0.666809 17.79 1.00014 17.0122 1.66681 16.3455C6.5557 11.0678 12.1946 7.10943 18.5835 4.47054C24.9724 1.83165 31.4446 0.512207 38.0001 0.512207C44.5557 0.512207 51.014 1.83165 57.3751 4.47054C63.7363 7.10943 69.389 11.0678 74.3335 16.3455C75.0001 17.0122 75.3335 17.79 75.3335 18.6789C75.3335 19.5678 75.0001 20.3455 74.3335 21.0122L66.6668 28.5122C66.0557 29.1233 65.3474 29.4567 64.5418 29.5122C63.7363 29.5678 63.0001 29.3455 62.3335 28.8455L52.6668 21.5122C52.2224 21.1789 51.889 20.79 51.6668 20.3455C51.4446 19.9011 51.3335 19.4011 51.3335 18.8455V9.34554C49.2224 8.67888 47.0557 8.1511 44.8335 7.76221C42.6113 7.37332 40.3335 7.17887 38.0001 7.17887C35.6668 7.17887 33.389 7.37332 31.1668 7.76221C28.9446 8.1511 26.7779 8.67888 24.6668 9.34554V18.8455C24.6668 19.4011 24.5557 19.9011 24.3335 20.3455C24.1113 20.79 23.7779 21.1789 23.3335 21.5122L13.6668 28.8455C13.0001 29.3455 12.264 29.5678 11.4585 29.5122C10.6529 29.4567 9.94459 29.1233 9.33348 28.5122ZM18.0001 11.6789C16.389 12.5122 14.8335 13.4705 13.3335 14.5539C11.8335 15.6372 10.2779 16.8455 8.66681 18.1789L12.0001 21.5122L18.0001 16.8455V11.6789ZM58.0001 11.8455V16.8455L64.0001 21.5122L67.3335 18.3455C65.7224 16.9011 64.1668 15.6511 62.6668 14.5955C61.1668 13.54 59.6113 12.6233 58.0001 11.8455Z"
11
+ fill="white"
12
+ />
13
+ </svg>
14
+ </section>
15
+ </div>
16
+