Skip to content

Commit dec3ccd

Browse files
author
AvidDollars
committed
create partially functioning area for practising typing
1 parent ef21417 commit dec3ccd

File tree

4 files changed

+127
-6
lines changed

4 files changed

+127
-6
lines changed
Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,38 @@
1-
<p>main-area works!</p>
1+
<!-- RETRIEVE FOCUS ON TEXTAREA -->
2+
{{ textareaFocus$ | async }}
3+
4+
<!-- VARIABLES -->
5+
@let error = textLoaderService.error();
6+
@let isLoading = textLoaderService.isLoading$ | async;
7+
@let statusCSS = "text-3xl centered-content";
8+
@let finished = finishedTyping();
9+
10+
<!-- HTML -->
11+
@if (isLoading) {
12+
<p [class]="statusCSS">loading...</p>
13+
}
14+
15+
@else if (error) {
16+
<p [class]="statusCSS">error...</p>
17+
}
18+
19+
@else if (finished) {
20+
<p [class]="statusCSS">show stats after the session</p>
21+
}
22+
23+
@else {
24+
<p
25+
#textElement
26+
class="opacity-35"
27+
[style.min-height.px]="hostElement.getBoundingClientRect().height"
28+
>{{ loadedText() }}</p>
29+
30+
<textarea
31+
#textarea
32+
class="p-2 border-0 absolute top-0 left-0 w-full outline-none resize-none no-scrollbar"
33+
spellcheck="false"
34+
autofocus
35+
[disabled]="finishedTyping()"
36+
[style.height.px]="textElement.getBoundingClientRect().height"
37+
></textarea>
38+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// TODO: disable text selection with "Ctrl + Shift + Left arrow" / "Ctrl + A"
2+
textarea::selection {
3+
background-color: transparent;
4+
}

frontend/src/app/main-area/main-area.component.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
22

33
import { MainAreaComponent } from './main-area.component';
4+
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
45

56
describe('MainAreaComponent', () => {
67
let component: MainAreaComponent;
78
let fixture: ComponentFixture<MainAreaComponent>;
89

910
beforeEach(async () => {
1011
await TestBed.configureTestingModule({
11-
imports: [MainAreaComponent]
12+
imports: [MainAreaComponent],
13+
providers: [
14+
provideExperimentalZonelessChangeDetection()
15+
]
1216
})
1317
.compileComponents();
1418

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,91 @@
1-
import { ChangeDetectionStrategy, Component } from '@angular/core';
1+
import { ChangeDetectionStrategy, Component, effect, ElementRef, inject, OnDestroy, OnInit, signal, viewChild } from '@angular/core';
2+
import { filter, fromEvent, map, scan, takeWhile, finalize, timer } from 'rxjs';
3+
import { AsyncPipe } from '@angular/common';
4+
import { TextLoaderService } from './text-loader.service';
5+
import { discardIrrelevantKeys, extractKey } from './utils';
6+
import JSConfetti from 'js-confetti';
7+
import { toObservable } from '@angular/core/rxjs-interop';
28

39
@Component({
410
selector: 'app-main-area',
5-
imports: [],
11+
imports: [AsyncPipe],
612
templateUrl: './main-area.component.html',
713
styleUrl: './main-area.component.scss',
814
changeDetection: ChangeDetectionStrategy.OnPush,
915
host: {
10-
class: "bg-bg-light col-start-2 col-span-4 row-span-5 rounded-xl p-2"
16+
class: `
17+
overflow-scroll no-scrollbar
18+
relative bg-light rounded-xl p-2
19+
col-start-3 col-span-2 row-span-5
20+
border-5 border-double border-secondary
21+
`,
22+
"[style.fontFamily]": "'Roboto Mono'",
1123
}
1224
})
13-
export class MainAreaComponent {
25+
export class MainAreaComponent implements OnInit, OnDestroy {
1426

27+
hostElement = inject(ElementRef).nativeElement as HTMLElement;
28+
textareaElement = viewChild<ElementRef<HTMLTextAreaElement>>("textarea");
29+
textareaFocus$ = toObservable(this.textareaElement).pipe( // to retrieve lost focus on textarea
30+
filter(value => value != undefined),
31+
map(textarea => textarea.nativeElement.focus()),
32+
);
33+
34+
// TODO: create confetti service
35+
confetti = new JSConfetti(); // confetti at the end of typing session
36+
37+
textLoaderService = inject(TextLoaderService);
38+
loadedText = this.textLoaderService.text;
39+
characterArray: string[] = [];
40+
finishedTyping = signal(false);
41+
42+
// TYPING STREAM
43+
typing$ = fromEvent<KeyboardEvent>(this.hostElement, "keydown").pipe(
44+
filter(discardIrrelevantKeys), // to filter out "Shift" / "CapsLock" / etc...
45+
map(extractKey),
46+
scan((acc, key) => {
47+
let index = (key === "Backspace") ? --acc[0] : ++acc[0];
48+
index = (index < 0) ? -1 : index; // min index: -1
49+
return [index, key] as [number, string];
50+
}, [-1, ""] as [number, string]),
51+
takeWhile(([index, _key]) => index < this.characterArray.length - 1), // TODO: you must add extra char to finish the session
52+
map(([index, char]) => {
53+
return [index, char, this.characterArray[index] === char] as [number, string, boolean];
54+
}),
55+
scan((error_counter, [_index, key, isCorrectKey]) => {
56+
if (!isCorrectKey && key !== "Backspace") {
57+
error_counter.set(key, (error_counter.get(key) ?? 0) + 1)
58+
};
59+
return error_counter;
60+
}, new Map(this.characterArray.map(char => [char, 0]))),
61+
finalize(() => {
62+
timer(200).subscribe(() => {
63+
this.finishedTyping.set(true);
64+
this.confetti.addConfetti();
65+
});
66+
timer(5000).subscribe(() => {
67+
this.confetti.destroyCanvas();
68+
})
69+
}),
70+
);
71+
72+
constructor() {
73+
// TODO: computed / linkedSignal instead?
74+
effect(() => { // new text -> new character array
75+
let text = this.loadedText() ?? "";
76+
this.characterArray = [...text];
77+
});
78+
}
79+
80+
ngOnInit() {
81+
this.hostElement.scrollTo({ top: 0, behavior: "smooth" });
82+
this.typing$.subscribe(v => console.log("v: ", v));
83+
84+
// TODO: autoscrolling
85+
//interval(50).pipe(delay(2_000)).subscribe(val => this.host.scrollTo({top: val / 5, behavior: "smooth"}))
86+
}
87+
88+
ngOnDestroy() {
89+
this.confetti.destroyCanvas();
90+
}
1591
}

0 commit comments

Comments
 (0)