Anupriya
commited on
Commit
·
04830fa
1
Parent(s):
cdc8731
shared button component
Browse files- src/app/app.module.ts +17 -15
- src/app/findword/findword.component.css +17 -153
- src/app/findword/findword.component.html +15 -68
- src/app/findword/findword.component.ts +12 -105
- src/app/footer/footer.component.css +6 -26
- src/app/home/home.component.css +1 -21
- src/app/listen/listen.component.css +33 -151
- src/app/listen/listen.component.html +46 -88
- src/app/listen/listen.component.ts +50 -141
- src/app/reading/reading.component.css +86 -376
- src/app/reading/reading.component.html +31 -174
- src/app/reading/reading.component.ts +28 -141
- src/app/vocabulary-builder/vocabulary-builder.component.css +1 -1
- src/app/vocabulary-builder/vocabulary-builder.component.html +25 -29
- src/app/vocabulary-builder/vocabulary-builder.component.ts +8 -12
- src/app/voice/voice.component.ts +3 -0
- src/app/writing/writing.component.css +13 -69
- src/app/writing/writing.component.html +10 -24
- src/app/writing/writing.component.ts +23 -29
src/app/app.module.ts
CHANGED
|
@@ -2,19 +2,19 @@ import { NgModule } from '@angular/core';
|
|
| 2 |
import { BrowserModule } from '@angular/platform-browser';
|
| 3 |
import { FormsModule } from '@angular/forms';
|
| 4 |
import { HttpClientModule } from '@angular/common/http';
|
|
|
|
| 5 |
import { AppRoutingModule } from './app-routing.module';
|
| 6 |
|
| 7 |
// Angular Material
|
| 8 |
import { MatDialogModule } from '@angular/material/dialog';
|
| 9 |
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
| 10 |
import { MatIconModule } from '@angular/material/icon';
|
|
|
|
| 11 |
|
| 12 |
// Components
|
| 13 |
import { AppComponent } from './app.component';
|
| 14 |
import { GenerateQuestionsComponent } from './generate-questions/generate-questions.component';
|
| 15 |
import { HomeComponent } from './home/home.component';
|
| 16 |
-
import { VoiceComponent } from './voice/voice.component';
|
| 17 |
-
import { ListenComponent } from './listen/listen.component';
|
| 18 |
import { WritingComponent } from './writing/writing.component';
|
| 19 |
import { VocabularyBuilderComponent } from './vocabulary-builder/vocabulary-builder.component';
|
| 20 |
import { FindwordComponent } from './findword/findword.component';
|
|
@@ -26,37 +26,39 @@ import { HeaderComponent } from './shared/header/header.component';
|
|
| 26 |
import { SignInComponent } from './sign-in/sign-in.component';
|
| 27 |
import { PronunciationComponent } from './pronunciation/pronunciation.component';
|
| 28 |
import { FooterComponent } from './footer/footer.component';
|
|
|
|
|
|
|
| 29 |
|
| 30 |
@NgModule({
|
| 31 |
declarations: [
|
| 32 |
-
AppComponent,
|
| 33 |
-
//GenerateQuestionsComponent,
|
| 34 |
HomeComponent,
|
| 35 |
-
VoiceComponent,
|
| 36 |
-
ListenComponent,
|
| 37 |
-
WritingComponent,
|
| 38 |
-
VocabularyBuilderComponent,
|
| 39 |
-
FindwordComponent,
|
| 40 |
-
ReadingComponent,
|
| 41 |
AuthenticationComponent,
|
| 42 |
AuthComponent,
|
| 43 |
RootRedirectComponent,
|
| 44 |
PronunciationComponent,
|
| 45 |
-
FooterComponent
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
],
|
| 48 |
imports: [
|
| 49 |
BrowserModule,
|
| 50 |
AppRoutingModule,
|
| 51 |
FormsModule,
|
| 52 |
HttpClientModule,
|
|
|
|
| 53 |
MatDialogModule,
|
| 54 |
MatSlideToggleModule,
|
| 55 |
MatIconModule,
|
| 56 |
-
|
| 57 |
-
SignInComponent // import the standalone sign-in (which embeds sign-up)
|
| 58 |
],
|
| 59 |
providers: [],
|
| 60 |
-
bootstrap: [AppComponent]
|
| 61 |
})
|
| 62 |
export class AppModule { }
|
|
|
|
| 2 |
import { BrowserModule } from '@angular/platform-browser';
|
| 3 |
import { FormsModule } from '@angular/forms';
|
| 4 |
import { HttpClientModule } from '@angular/common/http';
|
| 5 |
+
import { CommonModule } from '@angular/common';
|
| 6 |
import { AppRoutingModule } from './app-routing.module';
|
| 7 |
|
| 8 |
// Angular Material
|
| 9 |
import { MatDialogModule } from '@angular/material/dialog';
|
| 10 |
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
| 11 |
import { MatIconModule } from '@angular/material/icon';
|
| 12 |
+
import { MatCardModule } from '@angular/material/card';
|
| 13 |
|
| 14 |
// Components
|
| 15 |
import { AppComponent } from './app.component';
|
| 16 |
import { GenerateQuestionsComponent } from './generate-questions/generate-questions.component';
|
| 17 |
import { HomeComponent } from './home/home.component';
|
|
|
|
|
|
|
| 18 |
import { WritingComponent } from './writing/writing.component';
|
| 19 |
import { VocabularyBuilderComponent } from './vocabulary-builder/vocabulary-builder.component';
|
| 20 |
import { FindwordComponent } from './findword/findword.component';
|
|
|
|
| 26 |
import { SignInComponent } from './sign-in/sign-in.component';
|
| 27 |
import { PronunciationComponent } from './pronunciation/pronunciation.component';
|
| 28 |
import { FooterComponent } from './footer/footer.component';
|
| 29 |
+
// If you have AppButtonComponent, import it here as well
|
| 30 |
+
// import { AppButtonComponent } from './app-button/app-button.component';
|
| 31 |
|
| 32 |
@NgModule({
|
| 33 |
declarations: [
|
| 34 |
+
AppComponent,
|
|
|
|
| 35 |
HomeComponent,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
AuthenticationComponent,
|
| 37 |
AuthComponent,
|
| 38 |
RootRedirectComponent,
|
| 39 |
PronunciationComponent,
|
| 40 |
+
FooterComponent,
|
| 41 |
+
//GenerateQuestionsComponent,
|
| 42 |
+
//VocabularyBuilderComponent,
|
| 43 |
+
//FindwordComponent,
|
| 44 |
+
//HeaderComponent,
|
| 45 |
+
//SignInComponent,
|
| 46 |
+
//ReadingComponent,
|
| 47 |
+
//WritingComponent,
|
| 48 |
+
// AppButtonComponent
|
| 49 |
],
|
| 50 |
imports: [
|
| 51 |
BrowserModule,
|
| 52 |
AppRoutingModule,
|
| 53 |
FormsModule,
|
| 54 |
HttpClientModule,
|
| 55 |
+
CommonModule,
|
| 56 |
MatDialogModule,
|
| 57 |
MatSlideToggleModule,
|
| 58 |
MatIconModule,
|
| 59 |
+
MatCardModule
|
|
|
|
| 60 |
],
|
| 61 |
providers: [],
|
| 62 |
+
bootstrap: [AppComponent]
|
| 63 |
})
|
| 64 |
export class AppModule { }
|
src/app/findword/findword.component.css
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
/* General Styles */
|
| 2 |
body {
|
| 3 |
font-family: 'Roboto', sans-serif;
|
| 4 |
background-color: #f9f9f9;
|
|
@@ -6,15 +5,12 @@ body {
|
|
| 6 |
padding: 0;
|
| 7 |
}
|
| 8 |
|
| 9 |
-
|
| 10 |
-
/* Responsive Card */
|
| 11 |
.card1 {
|
| 12 |
background: #fff;
|
| 13 |
width: 80vw;
|
| 14 |
-
/*max-width: 1000px;*/
|
| 15 |
margin: 4vh auto;
|
| 16 |
padding: 2vw;
|
| 17 |
-
border: 10px solid
|
| 18 |
border-radius: 1vw;
|
| 19 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 20 |
position: absolute;
|
|
@@ -39,7 +35,6 @@ body {
|
|
| 39 |
|
| 40 |
.quiz-image {
|
| 41 |
width: 100%;
|
| 42 |
-
/*max-width: 350px;*/
|
| 43 |
height: auto;
|
| 44 |
border-radius: 10px;
|
| 45 |
}
|
|
@@ -82,8 +77,6 @@ h2 {
|
|
| 82 |
cursor: not-allowed;
|
| 83 |
}
|
| 84 |
|
| 85 |
-
/* Media Queries for Responsiveness */
|
| 86 |
-
|
| 87 |
@media (max-width: 768px) {
|
| 88 |
.content-container {
|
| 89 |
flex-direction: column;
|
|
@@ -116,7 +109,6 @@ h2 {
|
|
| 116 |
}
|
| 117 |
}
|
| 118 |
|
| 119 |
-
/* Game Screen */
|
| 120 |
.game-screen {
|
| 121 |
height: 71vh;
|
| 122 |
display: flex;
|
|
@@ -126,7 +118,7 @@ h2 {
|
|
| 126 |
width: 85vw;
|
| 127 |
margin: 4vh auto;
|
| 128 |
padding: 2vw;
|
| 129 |
-
border: 10px solid
|
| 130 |
border-radius: 1vw;
|
| 131 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 132 |
position: absolute;
|
|
@@ -135,7 +127,6 @@ h2 {
|
|
| 135 |
transform: translate(-50%, -50%);
|
| 136 |
}
|
| 137 |
|
| 138 |
-
/* Game Content Layout */
|
| 139 |
.game-content {
|
| 140 |
display: flex;
|
| 141 |
justify-content: space-between;
|
|
@@ -143,7 +134,6 @@ h2 {
|
|
| 143 |
align-items: center;
|
| 144 |
}
|
| 145 |
|
| 146 |
-
/* Cards */
|
| 147 |
.audio-card, .input-card {
|
| 148 |
width: 50%;
|
| 149 |
height: 20vw;
|
|
@@ -158,7 +148,6 @@ h2 {
|
|
| 158 |
align-items: center;
|
| 159 |
}
|
| 160 |
|
| 161 |
-
/* Input Wrapper */
|
| 162 |
.input-card {
|
| 163 |
display: flex;
|
| 164 |
flex-direction: column;
|
|
@@ -194,19 +183,16 @@ h2 {
|
|
| 194 |
opacity: 0.8;
|
| 195 |
}
|
| 196 |
|
| 197 |
-
/* Green border for correct input */
|
| 198 |
.input-wrapper input.correct {
|
| 199 |
-
border: 2px solid #4caf50 !important;
|
| 200 |
background-color: #e8f5e9;
|
| 201 |
}
|
| 202 |
|
| 203 |
-
/* Red border for incorrect input */
|
| 204 |
.input-wrapper input.error {
|
| 205 |
-
border: 2px solid #f44336 !important;
|
| 206 |
background-color: #ffebee;
|
| 207 |
}
|
| 208 |
|
| 209 |
-
|
| 210 |
.error-message {
|
| 211 |
color: #ff4d4d;
|
| 212 |
font-weight: bold;
|
|
@@ -215,30 +201,6 @@ h2 {
|
|
| 215 |
text-align: center;
|
| 216 |
}
|
| 217 |
|
| 218 |
-
/* Submit Button */
|
| 219 |
-
.submit-btn {
|
| 220 |
-
width: 85%;
|
| 221 |
-
background-color: #006780;
|
| 222 |
-
color: white;
|
| 223 |
-
font-size: 1rem;
|
| 224 |
-
padding: 1rem;
|
| 225 |
-
border: none;
|
| 226 |
-
border-radius: 8px;
|
| 227 |
-
cursor: pointer;
|
| 228 |
-
transition: background-color 0.3s;
|
| 229 |
-
text-align: center;
|
| 230 |
-
}
|
| 231 |
-
|
| 232 |
-
.submit-btn:hover {
|
| 233 |
-
background-color: #004d5c;
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
.submit-btn:disabled {
|
| 237 |
-
background-color: #cccccc;
|
| 238 |
-
cursor: not-allowed;
|
| 239 |
-
}
|
| 240 |
-
|
| 241 |
-
/* Action Buttons */
|
| 242 |
.action-buttons {
|
| 243 |
width: 100%;
|
| 244 |
display: flex;
|
|
@@ -290,7 +252,6 @@ h2 {
|
|
| 290 |
cursor: not-allowed;
|
| 291 |
}
|
| 292 |
|
| 293 |
-
/* Generate Button Section */
|
| 294 |
.generate-buttons {
|
| 295 |
display: flex;
|
| 296 |
gap: 10px;
|
|
@@ -332,7 +293,6 @@ h2 {
|
|
| 332 |
background-color: #e68900;
|
| 333 |
}
|
| 334 |
|
| 335 |
-
/* Responsive */
|
| 336 |
@media (max-width: 768px) {
|
| 337 |
.game-content {
|
| 338 |
flex-direction: column;
|
|
@@ -354,7 +314,6 @@ h2 {
|
|
| 354 |
}
|
| 355 |
}
|
| 356 |
|
| 357 |
-
/* Popup */
|
| 358 |
.popup-overlay {
|
| 359 |
position: fixed;
|
| 360 |
top: 0;
|
|
@@ -369,14 +328,16 @@ h2 {
|
|
| 369 |
}
|
| 370 |
|
| 371 |
.popup-content {
|
| 372 |
-
background-color: #
|
| 373 |
padding: 2vw;
|
| 374 |
border-radius: 8px;
|
| 375 |
text-align: center;
|
|
|
|
|
|
|
| 376 |
}
|
| 377 |
|
| 378 |
.popup-content p {
|
| 379 |
-
font-size:
|
| 380 |
margin-bottom: 2vw;
|
| 381 |
color: #004d5c;
|
| 382 |
font-weight: bold;
|
|
@@ -394,23 +355,6 @@ h2 {
|
|
| 394 |
}
|
| 395 |
}
|
| 396 |
|
| 397 |
-
.close-btn {
|
| 398 |
-
padding: 0.7vw;
|
| 399 |
-
font-size: 1vw;
|
| 400 |
-
background-color: #009688;
|
| 401 |
-
color: white;
|
| 402 |
-
border: none;
|
| 403 |
-
border-radius: 8px;
|
| 404 |
-
cursor: pointer;
|
| 405 |
-
transition: background-color 0.3s;
|
| 406 |
-
font-weight: bold;
|
| 407 |
-
}
|
| 408 |
-
|
| 409 |
-
.close-btn:hover {
|
| 410 |
-
background-color: #00796b;
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
/* Game Header */
|
| 414 |
.game-header {
|
| 415 |
position: relative;
|
| 416 |
width: 100%;
|
|
@@ -421,49 +365,25 @@ h2 {
|
|
| 421 |
}
|
| 422 |
|
| 423 |
.back-btn {
|
| 424 |
-
position: absolute;
|
| 425 |
left: 0;
|
| 426 |
top: 40%;
|
| 427 |
-
transform: translateY(-50%);
|
| 428 |
-
background: transparent;
|
| 429 |
-
border: none;
|
| 430 |
-
font-size: 2rem;
|
| 431 |
-
color: #006780;
|
| 432 |
-
cursor: pointer;
|
| 433 |
-
padding: 0 1rem;
|
| 434 |
-
transition: transform 0.3s;
|
| 435 |
width: 6vw;
|
| 436 |
}
|
| 437 |
|
| 438 |
-
.back-btn:hover {
|
| 439 |
-
transform: translateY(-50%) scale(1.1);
|
| 440 |
-
}
|
| 441 |
-
|
| 442 |
.game-title {
|
| 443 |
font-size: 2vw;
|
| 444 |
color: #004d5c;
|
| 445 |
-
/*font-family: 'Super Cartoon', sans-serif;*/
|
| 446 |
margin: 0;
|
| 447 |
text-align: center;
|
| 448 |
}
|
| 449 |
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
/* Optional: Modify the appearance of the disabled "Sentence" button specifically */
|
| 454 |
.info-btn:disabled {
|
| 455 |
-
background-color: #b0bec5;
|
| 456 |
-
color: #757575;
|
| 457 |
cursor: not-allowed;
|
| 458 |
opacity: 0.6;
|
| 459 |
}
|
| 460 |
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
/*step 2 left aside css start here*/
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
/* ===== Step 2: Left Audio Panel — Kid-friendly Player ===== */
|
| 467 |
.audio-card--kids {
|
| 468 |
--primary: #006780;
|
| 469 |
--accent: #009688;
|
|
@@ -525,7 +445,6 @@ h2 {
|
|
| 525 |
filter: grayscale(0.2);
|
| 526 |
}
|
| 527 |
|
| 528 |
-
/* Big circular play button */
|
| 529 |
.play-btn {
|
| 530 |
position: relative;
|
| 531 |
width: 65px;
|
|
@@ -591,7 +510,6 @@ h2 {
|
|
| 591 |
}
|
| 592 |
}
|
| 593 |
|
| 594 |
-
/* Wave shell (equalizer bars) */
|
| 595 |
.wave-shell {
|
| 596 |
display: flex;
|
| 597 |
align-items: flex-end;
|
|
@@ -608,7 +526,7 @@ h2 {
|
|
| 608 |
background: var(--bar);
|
| 609 |
border-radius: 4px;
|
| 610 |
animation: wave 1s ease-in-out infinite;
|
| 611 |
-
animation-play-state: paused;
|
| 612 |
}
|
| 613 |
|
| 614 |
.ac-player.is-playing .wave-shell .bar {
|
|
@@ -627,7 +545,6 @@ h2 {
|
|
| 627 |
}
|
| 628 |
}
|
| 629 |
|
| 630 |
-
/* Timeline / progress */
|
| 631 |
.timeline {
|
| 632 |
position: relative;
|
| 633 |
height: 10px;
|
|
@@ -651,7 +568,6 @@ h2 {
|
|
| 651 |
transition: width 0.15s linear;
|
| 652 |
}
|
| 653 |
|
| 654 |
-
/* Time labels */
|
| 655 |
.time {
|
| 656 |
width: 80%;
|
| 657 |
max-width: 520px;
|
|
@@ -661,7 +577,6 @@ h2 {
|
|
| 661 |
color: #004d5c;
|
| 662 |
}
|
| 663 |
|
| 664 |
-
/* Small helper text */
|
| 665 |
.ac-hint {
|
| 666 |
font-size: 0.95rem;
|
| 667 |
color: #004d5c;
|
|
@@ -671,7 +586,6 @@ h2 {
|
|
| 671 |
border-radius: 10px;
|
| 672 |
}
|
| 673 |
|
| 674 |
-
/* Responsive */
|
| 675 |
@media (max-width: 768px) {
|
| 676 |
.play-btn {
|
| 677 |
width: 84px;
|
|
@@ -692,11 +606,6 @@ h2 {
|
|
| 692 |
}
|
| 693 |
}
|
| 694 |
|
| 695 |
-
|
| 696 |
-
/*step =2 right aside css wstart herwe */
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
/* === Step 2: Right-side kid panel === */
|
| 700 |
.kid-panel {
|
| 701 |
--primary: #006780;
|
| 702 |
--accent: #009688;
|
|
@@ -711,7 +620,6 @@ h2 {
|
|
| 711 |
position: relative;
|
| 712 |
}
|
| 713 |
|
| 714 |
-
/* top row */
|
| 715 |
.kp-top {
|
| 716 |
display: flex;
|
| 717 |
align-items: center;
|
|
@@ -725,12 +633,10 @@ h2 {
|
|
| 725 |
font-weight: 800;
|
| 726 |
}
|
| 727 |
|
| 728 |
-
/* attempts (hearts) */
|
| 729 |
.kp-attempts .heart {
|
| 730 |
font-size: 1.15rem;
|
| 731 |
margin-left: 6px;
|
| 732 |
transition: opacity 0.25s ease, transform 0.25s ease;
|
| 733 |
-
|
| 734 |
}
|
| 735 |
|
| 736 |
.kp-attempts .heart.is-off {
|
|
@@ -738,7 +644,6 @@ h2 {
|
|
| 738 |
transform: scale(0.88);
|
| 739 |
}
|
| 740 |
|
| 741 |
-
/* input with subtle animation */
|
| 742 |
.kp-input-wrap {
|
| 743 |
position: relative;
|
| 744 |
display: grid;
|
|
@@ -766,7 +671,6 @@ h2 {
|
|
| 766 |
border-color: var(--accent);
|
| 767 |
}
|
| 768 |
|
| 769 |
-
/* success + error states (reuse your classes) */
|
| 770 |
.kp-input.correct {
|
| 771 |
border-color: #4caf50 !important;
|
| 772 |
background: #e8f5e9;
|
|
@@ -777,7 +681,6 @@ h2 {
|
|
| 777 |
background: #ffebee;
|
| 778 |
}
|
| 779 |
|
| 780 |
-
/* pop tick */
|
| 781 |
.ok-badge {
|
| 782 |
position: absolute;
|
| 783 |
right: 8%;
|
|
@@ -824,7 +727,6 @@ h2 {
|
|
| 824 |
}
|
| 825 |
}
|
| 826 |
|
| 827 |
-
/* subtle shake on wrong */
|
| 828 |
.kid-panel.is-shake .kp-input {
|
| 829 |
animation: kpShake 0.35s ease;
|
| 830 |
}
|
|
@@ -847,7 +749,6 @@ h2 {
|
|
| 847 |
}
|
| 848 |
}
|
| 849 |
|
| 850 |
-
/* primary submit re-using your color token */
|
| 851 |
.kp-submit {
|
| 852 |
width: 44%;
|
| 853 |
margin: 0 auto;
|
|
@@ -856,7 +757,6 @@ h2 {
|
|
| 856 |
font-size: 1.2vw;
|
| 857 |
}
|
| 858 |
|
| 859 |
-
/* info panel for meaning/example */
|
| 860 |
.info-panel {
|
| 861 |
background: #ffffffd9;
|
| 862 |
border: 1px dashed var(--accent);
|
|
@@ -891,7 +791,6 @@ h2 {
|
|
| 891 |
font-size: 0.98rem;
|
| 892 |
}
|
| 893 |
|
| 894 |
-
/* responsive tweaks */
|
| 895 |
@media (max-width: 768px) {
|
| 896 |
.kp-title {
|
| 897 |
font-size: 1.05rem;
|
|
@@ -911,8 +810,6 @@ h2 {
|
|
| 911 |
}
|
| 912 |
}
|
| 913 |
|
| 914 |
-
|
| 915 |
-
/* Shared action bar layout */
|
| 916 |
.action-bar {
|
| 917 |
display: flex;
|
| 918 |
justify-content: space-between;
|
|
@@ -927,14 +824,13 @@ h2 {
|
|
| 927 |
flex-wrap: wrap;
|
| 928 |
}
|
| 929 |
|
| 930 |
-
/* Base button using your spec */
|
| 931 |
.py-btn {
|
| 932 |
background-color: #006780;
|
| 933 |
color: #ffffff;
|
| 934 |
border: none;
|
| 935 |
padding: 15px 32px;
|
| 936 |
-
font-size: 1.2vw;
|
| 937 |
-
border-radius: 0.5vw;
|
| 938 |
cursor: pointer;
|
| 939 |
transition: background-color 0.3s, transform 0.3s;
|
| 940 |
font-weight: bold;
|
|
@@ -944,80 +840,49 @@ h2 {
|
|
| 944 |
line-height: 1;
|
| 945 |
}
|
| 946 |
|
| 947 |
-
/* Hover (your spec) */
|
| 948 |
.py-btn:hover:not(:disabled) {
|
| 949 |
background-color: #18788f;
|
| 950 |
transform: scale(1.05);
|
| 951 |
}
|
| 952 |
|
| 953 |
-
/* Press feedback */
|
| 954 |
.py-btn:active:not(:disabled) {
|
| 955 |
transform: scale(0.98);
|
| 956 |
}
|
| 957 |
|
| 958 |
-
/* Disabled */
|
| 959 |
.py-btn:disabled {
|
| 960 |
opacity: 0.55;
|
| 961 |
cursor: not-allowed;
|
| 962 |
transform: none;
|
| 963 |
}
|
| 964 |
|
| 965 |
-
/* Focus ring for keyboard users */
|
| 966 |
.py-btn:focus-visible {
|
| 967 |
outline: 3px solid #94c7d6;
|
| 968 |
outline-offset: 2px;
|
| 969 |
}
|
| 970 |
|
| 971 |
-
/* Optional tiny icon motion on hover */
|
| 972 |
.py-btn .btn__icon {
|
| 973 |
display: inline-block;
|
| 974 |
transform: translateY(0);
|
| 975 |
transition: transform 0.25s ease;
|
| 976 |
-
|
| 977 |
}
|
| 978 |
|
| 979 |
.py-btn:hover:not(:disabled) .btn__icon {
|
| 980 |
transform: translateY(-2px);
|
| 981 |
}
|
| 982 |
|
| 983 |
-
/* Small-screen fallback so 1.5vw does not get too small */
|
| 984 |
@media (max-width: 768px) {
|
| 985 |
.py-btn {
|
| 986 |
-
font-size: 16px;
|
| 987 |
-
border-radius: 10px;
|
| 988 |
padding: 12px 22px;
|
| 989 |
}
|
| 990 |
}
|
| 991 |
|
| 992 |
-
|
| 993 |
-
.
|
| 994 |
-
background-color: #009688;
|
| 995 |
-
color: black;
|
| 996 |
-
border: none;
|
| 997 |
-
padding: 0.4vw 0.9vw;
|
| 998 |
-
border-radius: 50%;
|
| 999 |
-
cursor: pointer;
|
| 1000 |
-
border: 1px solid black;
|
| 1001 |
-
width: 3vw;
|
| 1002 |
-
top: -5vw;
|
| 1003 |
right: -3.5vw;
|
| 1004 |
-
position: absolute;
|
| 1005 |
-
height: 3vw;
|
| 1006 |
-
font-size: 1.3vw;
|
| 1007 |
-
font-weight: bold;
|
| 1008 |
}
|
| 1009 |
|
| 1010 |
-
.close-btn1:hover {
|
| 1011 |
-
transform: scale(1.06);
|
| 1012 |
-
box-shadow: 0 6px 14px rgba(0,0,0,.22);
|
| 1013 |
-
background: #f7fafc;
|
| 1014 |
-
}
|
| 1015 |
-
|
| 1016 |
-
.close-btn1:active {
|
| 1017 |
-
transform: scale(0.98);
|
| 1018 |
-
}
|
| 1019 |
-
|
| 1020 |
-
/* Clean, static image at the left side (no blur, no animation) */
|
| 1021 |
.left-illustration {
|
| 1022 |
position: absolute;
|
| 1023 |
left: -2vw;
|
|
@@ -1033,7 +898,6 @@ h2 {
|
|
| 1033 |
image-rendering: crisp-edges;
|
| 1034 |
}
|
| 1035 |
|
| 1036 |
-
/* Smaller width on narrow screens */
|
| 1037 |
@media (max-width: 768px) {
|
| 1038 |
.left-illustration {
|
| 1039 |
width: 170px;
|
|
|
|
|
|
|
| 1 |
body {
|
| 2 |
font-family: 'Roboto', sans-serif;
|
| 3 |
background-color: #f9f9f9;
|
|
|
|
| 5 |
padding: 0;
|
| 6 |
}
|
| 7 |
|
|
|
|
|
|
|
| 8 |
.card1 {
|
| 9 |
background: #fff;
|
| 10 |
width: 80vw;
|
|
|
|
| 11 |
margin: 4vh auto;
|
| 12 |
padding: 2vw;
|
| 13 |
+
border: 10px solid var(--main-accent-color);
|
| 14 |
border-radius: 1vw;
|
| 15 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 16 |
position: absolute;
|
|
|
|
| 35 |
|
| 36 |
.quiz-image {
|
| 37 |
width: 100%;
|
|
|
|
| 38 |
height: auto;
|
| 39 |
border-radius: 10px;
|
| 40 |
}
|
|
|
|
| 77 |
cursor: not-allowed;
|
| 78 |
}
|
| 79 |
|
|
|
|
|
|
|
| 80 |
@media (max-width: 768px) {
|
| 81 |
.content-container {
|
| 82 |
flex-direction: column;
|
|
|
|
| 109 |
}
|
| 110 |
}
|
| 111 |
|
|
|
|
| 112 |
.game-screen {
|
| 113 |
height: 71vh;
|
| 114 |
display: flex;
|
|
|
|
| 118 |
width: 85vw;
|
| 119 |
margin: 4vh auto;
|
| 120 |
padding: 2vw;
|
| 121 |
+
border: 10px solid var(--main-accent-color);
|
| 122 |
border-radius: 1vw;
|
| 123 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 124 |
position: absolute;
|
|
|
|
| 127 |
transform: translate(-50%, -50%);
|
| 128 |
}
|
| 129 |
|
|
|
|
| 130 |
.game-content {
|
| 131 |
display: flex;
|
| 132 |
justify-content: space-between;
|
|
|
|
| 134 |
align-items: center;
|
| 135 |
}
|
| 136 |
|
|
|
|
| 137 |
.audio-card, .input-card {
|
| 138 |
width: 50%;
|
| 139 |
height: 20vw;
|
|
|
|
| 148 |
align-items: center;
|
| 149 |
}
|
| 150 |
|
|
|
|
| 151 |
.input-card {
|
| 152 |
display: flex;
|
| 153 |
flex-direction: column;
|
|
|
|
| 183 |
opacity: 0.8;
|
| 184 |
}
|
| 185 |
|
|
|
|
| 186 |
.input-wrapper input.correct {
|
| 187 |
+
border: 2px solid #4caf50 !important;
|
| 188 |
background-color: #e8f5e9;
|
| 189 |
}
|
| 190 |
|
|
|
|
| 191 |
.input-wrapper input.error {
|
| 192 |
+
border: 2px solid #f44336 !important;
|
| 193 |
background-color: #ffebee;
|
| 194 |
}
|
| 195 |
|
|
|
|
| 196 |
.error-message {
|
| 197 |
color: #ff4d4d;
|
| 198 |
font-weight: bold;
|
|
|
|
| 201 |
text-align: center;
|
| 202 |
}
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
.action-buttons {
|
| 205 |
width: 100%;
|
| 206 |
display: flex;
|
|
|
|
| 252 |
cursor: not-allowed;
|
| 253 |
}
|
| 254 |
|
|
|
|
| 255 |
.generate-buttons {
|
| 256 |
display: flex;
|
| 257 |
gap: 10px;
|
|
|
|
| 293 |
background-color: #e68900;
|
| 294 |
}
|
| 295 |
|
|
|
|
| 296 |
@media (max-width: 768px) {
|
| 297 |
.game-content {
|
| 298 |
flex-direction: column;
|
|
|
|
| 314 |
}
|
| 315 |
}
|
| 316 |
|
|
|
|
| 317 |
.popup-overlay {
|
| 318 |
position: fixed;
|
| 319 |
top: 0;
|
|
|
|
| 328 |
}
|
| 329 |
|
| 330 |
.popup-content {
|
| 331 |
+
background-color: #ffffff;
|
| 332 |
padding: 2vw;
|
| 333 |
border-radius: 8px;
|
| 334 |
text-align: center;
|
| 335 |
+
border: 10px solid var(--main-accent-color);
|
| 336 |
+
max-width: 50vw;
|
| 337 |
}
|
| 338 |
|
| 339 |
.popup-content p {
|
| 340 |
+
font-size: 1.3vw;
|
| 341 |
margin-bottom: 2vw;
|
| 342 |
color: #004d5c;
|
| 343 |
font-weight: bold;
|
|
|
|
| 355 |
}
|
| 356 |
}
|
| 357 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
.game-header {
|
| 359 |
position: relative;
|
| 360 |
width: 100%;
|
|
|
|
| 365 |
}
|
| 366 |
|
| 367 |
.back-btn {
|
|
|
|
| 368 |
left: 0;
|
| 369 |
top: 40%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
width: 6vw;
|
| 371 |
}
|
| 372 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
.game-title {
|
| 374 |
font-size: 2vw;
|
| 375 |
color: #004d5c;
|
|
|
|
| 376 |
margin: 0;
|
| 377 |
text-align: center;
|
| 378 |
}
|
| 379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
.info-btn:disabled {
|
| 381 |
+
background-color: #b0bec5;
|
| 382 |
+
color: #757575;
|
| 383 |
cursor: not-allowed;
|
| 384 |
opacity: 0.6;
|
| 385 |
}
|
| 386 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
.audio-card--kids {
|
| 388 |
--primary: #006780;
|
| 389 |
--accent: #009688;
|
|
|
|
| 445 |
filter: grayscale(0.2);
|
| 446 |
}
|
| 447 |
|
|
|
|
| 448 |
.play-btn {
|
| 449 |
position: relative;
|
| 450 |
width: 65px;
|
|
|
|
| 510 |
}
|
| 511 |
}
|
| 512 |
|
|
|
|
| 513 |
.wave-shell {
|
| 514 |
display: flex;
|
| 515 |
align-items: flex-end;
|
|
|
|
| 526 |
background: var(--bar);
|
| 527 |
border-radius: 4px;
|
| 528 |
animation: wave 1s ease-in-out infinite;
|
| 529 |
+
animation-play-state: paused;
|
| 530 |
}
|
| 531 |
|
| 532 |
.ac-player.is-playing .wave-shell .bar {
|
|
|
|
| 545 |
}
|
| 546 |
}
|
| 547 |
|
|
|
|
| 548 |
.timeline {
|
| 549 |
position: relative;
|
| 550 |
height: 10px;
|
|
|
|
| 568 |
transition: width 0.15s linear;
|
| 569 |
}
|
| 570 |
|
|
|
|
| 571 |
.time {
|
| 572 |
width: 80%;
|
| 573 |
max-width: 520px;
|
|
|
|
| 577 |
color: #004d5c;
|
| 578 |
}
|
| 579 |
|
|
|
|
| 580 |
.ac-hint {
|
| 581 |
font-size: 0.95rem;
|
| 582 |
color: #004d5c;
|
|
|
|
| 586 |
border-radius: 10px;
|
| 587 |
}
|
| 588 |
|
|
|
|
| 589 |
@media (max-width: 768px) {
|
| 590 |
.play-btn {
|
| 591 |
width: 84px;
|
|
|
|
| 606 |
}
|
| 607 |
}
|
| 608 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
.kid-panel {
|
| 610 |
--primary: #006780;
|
| 611 |
--accent: #009688;
|
|
|
|
| 620 |
position: relative;
|
| 621 |
}
|
| 622 |
|
|
|
|
| 623 |
.kp-top {
|
| 624 |
display: flex;
|
| 625 |
align-items: center;
|
|
|
|
| 633 |
font-weight: 800;
|
| 634 |
}
|
| 635 |
|
|
|
|
| 636 |
.kp-attempts .heart {
|
| 637 |
font-size: 1.15rem;
|
| 638 |
margin-left: 6px;
|
| 639 |
transition: opacity 0.25s ease, transform 0.25s ease;
|
|
|
|
| 640 |
}
|
| 641 |
|
| 642 |
.kp-attempts .heart.is-off {
|
|
|
|
| 644 |
transform: scale(0.88);
|
| 645 |
}
|
| 646 |
|
|
|
|
| 647 |
.kp-input-wrap {
|
| 648 |
position: relative;
|
| 649 |
display: grid;
|
|
|
|
| 671 |
border-color: var(--accent);
|
| 672 |
}
|
| 673 |
|
|
|
|
| 674 |
.kp-input.correct {
|
| 675 |
border-color: #4caf50 !important;
|
| 676 |
background: #e8f5e9;
|
|
|
|
| 681 |
background: #ffebee;
|
| 682 |
}
|
| 683 |
|
|
|
|
| 684 |
.ok-badge {
|
| 685 |
position: absolute;
|
| 686 |
right: 8%;
|
|
|
|
| 727 |
}
|
| 728 |
}
|
| 729 |
|
|
|
|
| 730 |
.kid-panel.is-shake .kp-input {
|
| 731 |
animation: kpShake 0.35s ease;
|
| 732 |
}
|
|
|
|
| 749 |
}
|
| 750 |
}
|
| 751 |
|
|
|
|
| 752 |
.kp-submit {
|
| 753 |
width: 44%;
|
| 754 |
margin: 0 auto;
|
|
|
|
| 757 |
font-size: 1.2vw;
|
| 758 |
}
|
| 759 |
|
|
|
|
| 760 |
.info-panel {
|
| 761 |
background: #ffffffd9;
|
| 762 |
border: 1px dashed var(--accent);
|
|
|
|
| 791 |
font-size: 0.98rem;
|
| 792 |
}
|
| 793 |
|
|
|
|
| 794 |
@media (max-width: 768px) {
|
| 795 |
.kp-title {
|
| 796 |
font-size: 1.05rem;
|
|
|
|
| 810 |
}
|
| 811 |
}
|
| 812 |
|
|
|
|
|
|
|
| 813 |
.action-bar {
|
| 814 |
display: flex;
|
| 815 |
justify-content: space-between;
|
|
|
|
| 824 |
flex-wrap: wrap;
|
| 825 |
}
|
| 826 |
|
|
|
|
| 827 |
.py-btn {
|
| 828 |
background-color: #006780;
|
| 829 |
color: #ffffff;
|
| 830 |
border: none;
|
| 831 |
padding: 15px 32px;
|
| 832 |
+
font-size: 1.2vw;
|
| 833 |
+
border-radius: 0.5vw;
|
| 834 |
cursor: pointer;
|
| 835 |
transition: background-color 0.3s, transform 0.3s;
|
| 836 |
font-weight: bold;
|
|
|
|
| 840 |
line-height: 1;
|
| 841 |
}
|
| 842 |
|
|
|
|
| 843 |
.py-btn:hover:not(:disabled) {
|
| 844 |
background-color: #18788f;
|
| 845 |
transform: scale(1.05);
|
| 846 |
}
|
| 847 |
|
|
|
|
| 848 |
.py-btn:active:not(:disabled) {
|
| 849 |
transform: scale(0.98);
|
| 850 |
}
|
| 851 |
|
|
|
|
| 852 |
.py-btn:disabled {
|
| 853 |
opacity: 0.55;
|
| 854 |
cursor: not-allowed;
|
| 855 |
transform: none;
|
| 856 |
}
|
| 857 |
|
|
|
|
| 858 |
.py-btn:focus-visible {
|
| 859 |
outline: 3px solid #94c7d6;
|
| 860 |
outline-offset: 2px;
|
| 861 |
}
|
| 862 |
|
|
|
|
| 863 |
.py-btn .btn__icon {
|
| 864 |
display: inline-block;
|
| 865 |
transform: translateY(0);
|
| 866 |
transition: transform 0.25s ease;
|
|
|
|
| 867 |
}
|
| 868 |
|
| 869 |
.py-btn:hover:not(:disabled) .btn__icon {
|
| 870 |
transform: translateY(-2px);
|
| 871 |
}
|
| 872 |
|
|
|
|
| 873 |
@media (max-width: 768px) {
|
| 874 |
.py-btn {
|
| 875 |
+
font-size: 16px;
|
| 876 |
+
border-radius: 10px;
|
| 877 |
padding: 12px 22px;
|
| 878 |
}
|
| 879 |
}
|
| 880 |
|
| 881 |
+
.user-guide-close-icon {
|
| 882 |
+
top: -5.4vw;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 883 |
right: -3.5vw;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 884 |
}
|
| 885 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 886 |
.left-illustration {
|
| 887 |
position: absolute;
|
| 888 |
left: -2vw;
|
|
|
|
| 898 |
image-rendering: crisp-edges;
|
| 899 |
}
|
| 900 |
|
|
|
|
| 901 |
@media (max-width: 768px) {
|
| 902 |
.left-illustration {
|
| 903 |
width: 170px;
|
src/app/findword/findword.component.html
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
<div class="full-container">
|
| 2 |
<app-header [title]="'Find the Word'"></app-header>
|
| 3 |
|
| 4 |
-
|
| 5 |
<img src="assets/images/grammar-bg.png" alt="Chat Background" class="grammar-bg" />
|
| 6 |
|
| 7 |
<div class="findword-container">
|
|
@@ -18,25 +17,19 @@
|
|
| 18 |
If needed, they can listen again. The exercise also has buttons to show the word’s meaning and an example sentence.
|
| 19 |
A reset option lets students try again. This activity helps students practice listening and writing skills in an easy and engaging way, making learning more enjoyable and effective.
|
| 20 |
</p>
|
| 21 |
-
<button (click)="startGame()"
|
| 22 |
</div>
|
| 23 |
</div>
|
| 24 |
</div>
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
<div *ngIf="step === 2" class="game-screen">
|
| 29 |
<div class="game-header">
|
| 30 |
<img src="assets/images/back.png" alt="Go Back" class="back-btn" (click)="goBack()" />
|
| 31 |
<h2 class="game-title">Listen & Type</h2>
|
| 32 |
-
|
| 33 |
-
<button class="close-btn1" (click)=" closeToStart()" aria-label="Close">✖</button>
|
| 34 |
</div>
|
| 35 |
|
| 36 |
<div class="game-content">
|
| 37 |
-
|
| 38 |
-
<!-- Audio Section -->
|
| 39 |
-
<!-- Audio Section (Left) -->
|
| 40 |
<div class="audio-card audio-card--kids">
|
| 41 |
<div class="ac-header">
|
| 42 |
<button (click)="fetchAudio()"
|
|
@@ -44,22 +37,18 @@
|
|
| 44 |
class="btn generate-btn ac-generate"
|
| 45 |
aria-label="Generate a new question">
|
| 46 |
<span *ngIf="!isLoading">Generate audio</span>
|
| 47 |
-
<!-- <span *ngIf="isLoading" class="spinner" aria-hidden="true"></span>-->
|
| 48 |
<span *ngIf="isLoading">Generating</span>
|
| 49 |
</button>
|
| 50 |
</div>
|
| 51 |
-
<!-- Left mascot (decorative) -->
|
| 52 |
<img class="left-illustration"
|
| 53 |
src="assets/images/find_word/audio.png"
|
| 54 |
alt="" />
|
| 55 |
-
<!-- Player Shell -->
|
| 56 |
<div class="ac-player" [class.is-disabled]="!audioUrl" [class.is-playing]="isPlaying">
|
| 57 |
<button class="play-btn"
|
| 58 |
[disabled]="!audioUrl"
|
| 59 |
(click)="togglePlayback()"
|
| 60 |
[attr.aria-pressed]="isPlaying"
|
| 61 |
[attr.aria-label]="isPlaying ? 'Pause audio' : 'Play audio'">
|
| 62 |
-
<!-- Inline SVG icons (no external deps) -->
|
| 63 |
<svg *ngIf="!isPlaying" viewBox="0 0 24 24" class="icon">
|
| 64 |
<path d="M8 5v14l11-7z"></path>
|
| 65 |
</svg>
|
|
@@ -69,25 +58,19 @@
|
|
| 69 |
<span class="pulse" aria-hidden="true"></span>
|
| 70 |
</button>
|
| 71 |
|
| 72 |
-
|
| 73 |
-
<!-- Wave shell (animated bars while playing) -->
|
| 74 |
<div class="wave-shell" aria-hidden="true">
|
| 75 |
<span class="bar" *ngFor="let b of bars; let i = index" [style.animation-delay.ms]="(i%6)*120"></span>
|
| 76 |
</div>
|
| 77 |
|
| 78 |
-
<!-- Timeline -->
|
| 79 |
<div class="timeline" [class.is-disabled]="!audioUrl">
|
| 80 |
<div class="progress" [style.width.%]="progress"></div>
|
| 81 |
</div>
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
<div class="ac-hint" *ngIf="!audioUrl">
|
| 86 |
Tap <strong>Generate audio</strong> to load the audio.
|
| 87 |
</div>
|
| 88 |
</div>
|
| 89 |
|
| 90 |
-
<!-- Keep the audio element; wire player events -->
|
| 91 |
<audio #audioPlayer
|
| 92 |
(timeupdate)="onTimeUpdate()"
|
| 93 |
(ended)="onAudioEnded()"
|
|
@@ -98,30 +81,17 @@
|
|
| 98 |
</audio>
|
| 99 |
</div>
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
<!-- RIGHT SIDE: Kid-friendly input panel -->
|
| 108 |
<div class="input-card kid-panel"
|
| 109 |
[class.is-correct]="isCorrect"
|
| 110 |
[class.is-shake]="ui.shake">
|
| 111 |
-
|
| 112 |
-
<!-- Top row: title + attempts -->
|
| 113 |
<div class="kp-top">
|
| 114 |
<h3 class="kp-title">Type the word</h3>
|
| 115 |
-
|
| 116 |
-
<!-- Attempts with hearts -->
|
| 117 |
<div class="kp-attempts" aria-label="Attempts left">
|
| 118 |
<span class="heart" [class.is-off]="attemptsLeft < 1"></span>
|
| 119 |
<span class="heart" [class.is-off]="attemptsLeft < 2"></span>
|
| 120 |
<span class="heart" [class.is-off]="attemptsLeft < 3"></span>
|
| 121 |
</div>
|
| 122 |
</div>
|
| 123 |
-
|
| 124 |
-
<!-- Input + success badge -->
|
| 125 |
<div class="kp-input-wrap">
|
| 126 |
<input type="text"
|
| 127 |
class="kp-input"
|
|
@@ -130,8 +100,6 @@
|
|
| 130 |
(ngModelChange)="onInputChange()"
|
| 131 |
placeholder="Type what you heard"
|
| 132 |
[ngClass]="{ 'correct': isCorrect, 'error': !isCorrect && validationMessage }" />
|
| 133 |
-
|
| 134 |
-
<!-- Pop tick on correct -->
|
| 135 |
<div class="ok-badge" *ngIf="isCorrect || ui?.pulseOk" aria-hidden="true">
|
| 136 |
<svg viewBox="0 0 24 24" class="ok-icon">
|
| 137 |
<circle cx="12" cy="12" r="10"></circle>
|
|
@@ -139,61 +107,40 @@
|
|
| 139 |
</svg>
|
| 140 |
</div>
|
| 141 |
</div>
|
| 142 |
-
|
| 143 |
-
<!-- Validation message (only when needed) -->
|
| 144 |
<p *ngIf="validationMessage" class="error-message">{{ validationMessage }}</p>
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
<button (click)="validateWord()"
|
| 148 |
-
[disabled]="!canSubmit || attemptsLeft === 0"
|
| 149 |
-
class="btn submit-btn kp-submit">
|
| 150 |
Submit
|
| 151 |
-
</button>
|
| 152 |
-
|
| 153 |
<div class="action-bar">
|
| 154 |
<div class="left-buttons">
|
| 155 |
-
<button
|
| 156 |
-
|
| 157 |
-
[disabled]="isMeaningButtonDisabled">
|
| 158 |
<span class="btn__icon" aria-hidden="true">📘</span>
|
| 159 |
<span>Meaning</span>
|
| 160 |
-
</button>
|
| 161 |
|
| 162 |
-
<button
|
| 163 |
-
|
| 164 |
-
[disabled]="isExampleButtonDisabled">
|
| 165 |
<span class="btn__icon" aria-hidden="true">✍️</span>
|
| 166 |
<span>Example</span>
|
| 167 |
-
</button>
|
| 168 |
</div>
|
| 169 |
|
| 170 |
-
<button
|
| 171 |
-
|
| 172 |
-
[disabled]="attemptsLeft > 0 && !isCorrect">
|
| 173 |
<span class="btn__icon" aria-hidden="true">⟲</span>
|
| 174 |
<span>Reset</span>
|
| 175 |
-
</button>
|
| 176 |
</div>
|
| 177 |
-
|
| 178 |
-
|
| 179 |
</div>
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
</div>
|
| 188 |
</div>
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
<div *ngIf="isPopupVisible" class="popup-overlay">
|
| 194 |
<div class="popup-content">
|
| 195 |
<p>{{ popupMessage }}</p>
|
| 196 |
-
<button
|
| 197 |
</div>
|
| 198 |
</div>
|
| 199 |
</div>
|
|
|
|
| 1 |
<div class="full-container">
|
| 2 |
<app-header [title]="'Find the Word'"></app-header>
|
| 3 |
|
|
|
|
| 4 |
<img src="assets/images/grammar-bg.png" alt="Chat Background" class="grammar-bg" />
|
| 5 |
|
| 6 |
<div class="findword-container">
|
|
|
|
| 17 |
If needed, they can listen again. The exercise also has buttons to show the word’s meaning and an example sentence.
|
| 18 |
A reset option lets students try again. This activity helps students practice listening and writing skills in an easy and engaging way, making learning more enjoyable and effective.
|
| 19 |
</p>
|
| 20 |
+
<app-button (click)="startGame()">Start Learning</app-button>
|
| 21 |
</div>
|
| 22 |
</div>
|
| 23 |
</div>
|
| 24 |
|
|
|
|
|
|
|
| 25 |
<div *ngIf="step === 2" class="game-screen">
|
| 26 |
<div class="game-header">
|
| 27 |
<img src="assets/images/back.png" alt="Go Back" class="back-btn" (click)="goBack()" />
|
| 28 |
<h2 class="game-title">Listen & Type</h2>
|
| 29 |
+
<button class="user-guide-close-icon" (click)=" closeToStart()">×</button>
|
|
|
|
| 30 |
</div>
|
| 31 |
|
| 32 |
<div class="game-content">
|
|
|
|
|
|
|
|
|
|
| 33 |
<div class="audio-card audio-card--kids">
|
| 34 |
<div class="ac-header">
|
| 35 |
<button (click)="fetchAudio()"
|
|
|
|
| 37 |
class="btn generate-btn ac-generate"
|
| 38 |
aria-label="Generate a new question">
|
| 39 |
<span *ngIf="!isLoading">Generate audio</span>
|
|
|
|
| 40 |
<span *ngIf="isLoading">Generating</span>
|
| 41 |
</button>
|
| 42 |
</div>
|
|
|
|
| 43 |
<img class="left-illustration"
|
| 44 |
src="assets/images/find_word/audio.png"
|
| 45 |
alt="" />
|
|
|
|
| 46 |
<div class="ac-player" [class.is-disabled]="!audioUrl" [class.is-playing]="isPlaying">
|
| 47 |
<button class="play-btn"
|
| 48 |
[disabled]="!audioUrl"
|
| 49 |
(click)="togglePlayback()"
|
| 50 |
[attr.aria-pressed]="isPlaying"
|
| 51 |
[attr.aria-label]="isPlaying ? 'Pause audio' : 'Play audio'">
|
|
|
|
| 52 |
<svg *ngIf="!isPlaying" viewBox="0 0 24 24" class="icon">
|
| 53 |
<path d="M8 5v14l11-7z"></path>
|
| 54 |
</svg>
|
|
|
|
| 58 |
<span class="pulse" aria-hidden="true"></span>
|
| 59 |
</button>
|
| 60 |
|
|
|
|
|
|
|
| 61 |
<div class="wave-shell" aria-hidden="true">
|
| 62 |
<span class="bar" *ngFor="let b of bars; let i = index" [style.animation-delay.ms]="(i%6)*120"></span>
|
| 63 |
</div>
|
| 64 |
|
|
|
|
| 65 |
<div class="timeline" [class.is-disabled]="!audioUrl">
|
| 66 |
<div class="progress" [style.width.%]="progress"></div>
|
| 67 |
</div>
|
| 68 |
|
|
|
|
|
|
|
| 69 |
<div class="ac-hint" *ngIf="!audioUrl">
|
| 70 |
Tap <strong>Generate audio</strong> to load the audio.
|
| 71 |
</div>
|
| 72 |
</div>
|
| 73 |
|
|
|
|
| 74 |
<audio #audioPlayer
|
| 75 |
(timeupdate)="onTimeUpdate()"
|
| 76 |
(ended)="onAudioEnded()"
|
|
|
|
| 81 |
</audio>
|
| 82 |
</div>
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
<div class="input-card kid-panel"
|
| 85 |
[class.is-correct]="isCorrect"
|
| 86 |
[class.is-shake]="ui.shake">
|
|
|
|
|
|
|
| 87 |
<div class="kp-top">
|
| 88 |
<h3 class="kp-title">Type the word</h3>
|
|
|
|
|
|
|
| 89 |
<div class="kp-attempts" aria-label="Attempts left">
|
| 90 |
<span class="heart" [class.is-off]="attemptsLeft < 1"></span>
|
| 91 |
<span class="heart" [class.is-off]="attemptsLeft < 2"></span>
|
| 92 |
<span class="heart" [class.is-off]="attemptsLeft < 3"></span>
|
| 93 |
</div>
|
| 94 |
</div>
|
|
|
|
|
|
|
| 95 |
<div class="kp-input-wrap">
|
| 96 |
<input type="text"
|
| 97 |
class="kp-input"
|
|
|
|
| 100 |
(ngModelChange)="onInputChange()"
|
| 101 |
placeholder="Type what you heard"
|
| 102 |
[ngClass]="{ 'correct': isCorrect, 'error': !isCorrect && validationMessage }" />
|
|
|
|
|
|
|
| 103 |
<div class="ok-badge" *ngIf="isCorrect || ui?.pulseOk" aria-hidden="true">
|
| 104 |
<svg viewBox="0 0 24 24" class="ok-icon">
|
| 105 |
<circle cx="12" cy="12" r="10"></circle>
|
|
|
|
| 107 |
</svg>
|
| 108 |
</div>
|
| 109 |
</div>
|
|
|
|
|
|
|
| 110 |
<p *ngIf="validationMessage" class="error-message">{{ validationMessage }}</p>
|
| 111 |
+
<app-button (click)="validateWord()"
|
| 112 |
+
[disabled]="!canSubmit || attemptsLeft === 0">
|
|
|
|
|
|
|
|
|
|
| 113 |
Submit
|
| 114 |
+
</app-button>
|
|
|
|
| 115 |
<div class="action-bar">
|
| 116 |
<div class="left-buttons">
|
| 117 |
+
<app-button (click)="showMeaningPanel()"
|
| 118 |
+
[disabled]="isMeaningButtonDisabled">
|
|
|
|
| 119 |
<span class="btn__icon" aria-hidden="true">📘</span>
|
| 120 |
<span>Meaning</span>
|
| 121 |
+
</app-button>
|
| 122 |
|
| 123 |
+
<app-button (click)="showSentencePanel()"
|
| 124 |
+
[disabled]="isExampleButtonDisabled">
|
|
|
|
| 125 |
<span class="btn__icon" aria-hidden="true">✍️</span>
|
| 126 |
<span>Example</span>
|
| 127 |
+
</app-button>
|
| 128 |
</div>
|
| 129 |
|
| 130 |
+
<app-button (click)="nextQuestion()"
|
| 131 |
+
[disabled]="attemptsLeft > 0 && !isCorrect">
|
|
|
|
| 132 |
<span class="btn__icon" aria-hidden="true">⟲</span>
|
| 133 |
<span>Reset</span>
|
| 134 |
+
</app-button>
|
| 135 |
</div>
|
|
|
|
|
|
|
| 136 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
</div>
|
| 138 |
</div>
|
| 139 |
|
|
|
|
|
|
|
|
|
|
| 140 |
<div *ngIf="isPopupVisible" class="popup-overlay">
|
| 141 |
<div class="popup-content">
|
| 142 |
<p>{{ popupMessage }}</p>
|
| 143 |
+
<app-button (click)="closePopup()">Close</app-button>
|
| 144 |
</div>
|
| 145 |
</div>
|
| 146 |
</div>
|
src/app/findword/findword.component.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
| 1 |
import { Component, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
|
| 2 |
import { FindwordService } from '../findword/findword.service';
|
| 3 |
import { Router } from '@angular/router';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
@Component({
|
| 6 |
selector: 'app-findword',
|
| 7 |
templateUrl: './findword.component.html',
|
| 8 |
-
styleUrl: './findword.component.css'
|
|
|
|
|
|
|
| 9 |
})
|
| 10 |
export class FindwordComponent {
|
| 11 |
@ViewChild('audioPlayer', { static: false }) audioPlayer?: ElementRef<HTMLAudioElement>;
|
| 12 |
@ViewChild('audioPlayer') audioRef!: ElementRef<HTMLAudioElement>;
|
| 13 |
|
| 14 |
-
|
| 15 |
step: number = 1;
|
| 16 |
progress: number = 0;
|
| 17 |
audioUrl: string | null = null;
|
|
@@ -31,8 +36,6 @@ export class FindwordComponent {
|
|
| 31 |
audioFinished: boolean = false;
|
| 32 |
canSubmit: boolean = false;
|
| 33 |
isPlaying: boolean = false;
|
| 34 |
-
|
| 35 |
-
|
| 36 |
currentTimeDisplay = '0:00';
|
| 37 |
durationDisplay = '0:00';
|
| 38 |
constructor(
|
|
@@ -41,7 +44,6 @@ export class FindwordComponent {
|
|
| 41 |
private router: Router
|
| 42 |
) { }
|
| 43 |
|
| 44 |
-
// Fetch audio from backend
|
| 45 |
fetchAudio() {
|
| 46 |
this.isLoading = true;
|
| 47 |
this.correctWord = null;
|
|
@@ -69,10 +71,7 @@ export class FindwordComponent {
|
|
| 69 |
this.wordSentence = response.sentence;
|
| 70 |
console.log("✅ Correct Word Set:", this.correctWord);
|
| 71 |
|
| 72 |
-
// ✅ use service helper (works on local & HF)
|
| 73 |
this.audioUrl = this.findwordService.buildAssetUrl(response.audio_file_path);
|
| 74 |
-
|
| 75 |
-
|
| 76 |
this.cd.detectChanges();
|
| 77 |
|
| 78 |
setTimeout(() => {
|
|
@@ -122,61 +121,8 @@ export class FindwordComponent {
|
|
| 122 |
this.isPopupVisible = true;
|
| 123 |
}
|
| 124 |
|
| 125 |
-
//validateWord() {
|
| 126 |
-
// if (this.attemptsLeft <= 0) {
|
| 127 |
-
// // Show Meaning and Example buttons after all attempts are used
|
| 128 |
-
// this.showMeaning = true;
|
| 129 |
-
// this.showSentence = true;
|
| 130 |
-
// return;
|
| 131 |
-
// }
|
| 132 |
-
|
| 133 |
-
// console.log(`📝 Validating answer: ${this.userInput} (Attempts left: ${this.attemptsLeft})`);
|
| 134 |
-
|
| 135 |
-
// if (!this.correctWord) {
|
| 136 |
-
// console.error("❌ Error: No correct word set for validation!");
|
| 137 |
-
// this.validationMessage = 'Error validating word: Missing correct word.';
|
| 138 |
-
// this.showMeaning = true;
|
| 139 |
-
// this.showSentence = true;
|
| 140 |
-
// return;
|
| 141 |
-
// }
|
| 142 |
-
|
| 143 |
-
// const requestData = {
|
| 144 |
-
// user_input: this.userInput.trim(),
|
| 145 |
-
// correct_word: this.correctWord.trim().replace('.', '')
|
| 146 |
-
// };
|
| 147 |
-
// console.log("📤 Sending validation request:", requestData);
|
| 148 |
-
|
| 149 |
-
// this.findwordService.validateWord(this.userInput, this.correctWord).subscribe(response => {
|
| 150 |
-
// console.log("📥 Validation Response:", response);
|
| 151 |
-
|
| 152 |
-
// if (response.status === 'success') {
|
| 153 |
-
// this.validationMessage = "✅ Correct!";
|
| 154 |
-
// this.isCorrect = true;
|
| 155 |
-
// this.attemptsLeft = 0;
|
| 156 |
-
// this.popupMessage = this.validationMessage;
|
| 157 |
-
// this.isGenerateDisabled = true;
|
| 158 |
-
// } else {
|
| 159 |
-
// this.attemptsLeft--;
|
| 160 |
-
// if (this.attemptsLeft === 0) {
|
| 161 |
-
// this.validationMessage = `❌ Incorrect! The correct word was '${this.correctWord}'.`;
|
| 162 |
-
// this.correctWord = response.correct_word;
|
| 163 |
-
// this.isGenerateDisabled = true;
|
| 164 |
-
// this.showMeaning = true;
|
| 165 |
-
// this.showSentence = true;
|
| 166 |
-
// } else {
|
| 167 |
-
// this.validationMessage = `❌ Incorrect! You have ${this.attemptsLeft} attempt(s) left.`;
|
| 168 |
-
// }
|
| 169 |
-
// this.isCorrect = false;
|
| 170 |
-
// }
|
| 171 |
-
// }, error => {
|
| 172 |
-
// console.error("❌ Validation API Error:", error);
|
| 173 |
-
// this.validationMessage = 'Error validating word: API failed.';
|
| 174 |
-
// });
|
| 175 |
-
//}
|
| 176 |
-
|
| 177 |
validateWord() {
|
| 178 |
if (this.attemptsLeft <= 0) {
|
| 179 |
-
// Show Meaning and Example buttons after all attempts are used
|
| 180 |
this.showMeaning = true;
|
| 181 |
this.showSentence = true;
|
| 182 |
return;
|
|
@@ -204,11 +150,8 @@ export class FindwordComponent {
|
|
| 204 |
if (response.status === 'success') {
|
| 205 |
this.validationMessage = "✅ Correct!";
|
| 206 |
this.isCorrect = true;
|
| 207 |
-
|
| 208 |
-
// ⬅ added: success pulse animation
|
| 209 |
this.ui.pulseOk = true;
|
| 210 |
setTimeout(() => (this.ui.pulseOk = false), 1200);
|
| 211 |
-
|
| 212 |
this.attemptsLeft = 0;
|
| 213 |
this.popupMessage = this.validationMessage;
|
| 214 |
this.isGenerateDisabled = true;
|
|
@@ -224,8 +167,6 @@ export class FindwordComponent {
|
|
| 224 |
this.validationMessage = `❌ Incorrect! You have ${this.attemptsLeft} attempt(s) left.`;
|
| 225 |
}
|
| 226 |
this.isCorrect = false;
|
| 227 |
-
|
| 228 |
-
// ⬅ added: gentle shake on wrong answer
|
| 229 |
this.ui.shake = true;
|
| 230 |
setTimeout(() => (this.ui.shake = false), 400);
|
| 231 |
}
|
|
@@ -235,7 +176,6 @@ export class FindwordComponent {
|
|
| 235 |
});
|
| 236 |
}
|
| 237 |
|
| 238 |
-
|
| 239 |
onAudioReady() {
|
| 240 |
console.log("🎧 Audio is ready, trying to play...");
|
| 241 |
if (this.audioPlayer) {
|
|
@@ -251,11 +191,6 @@ export class FindwordComponent {
|
|
| 251 |
this.audioPlayer.nativeElement.pause();
|
| 252 |
}
|
| 253 |
}
|
| 254 |
-
//playAudio() {
|
| 255 |
-
// if (!this.audioUrl) {
|
| 256 |
-
// console.error("❌ Error: Audio URL is empty!");
|
| 257 |
-
// return;
|
| 258 |
-
// }
|
| 259 |
playAudio() {
|
| 260 |
if (this.audioPlayer?.nativeElement) {
|
| 261 |
this.audioPlayer.nativeElement.play();
|
|
@@ -281,8 +216,8 @@ export class FindwordComponent {
|
|
| 281 |
this.validationMessage = '';
|
| 282 |
this.attemptsLeft = 3;
|
| 283 |
this.correctWord = null;
|
| 284 |
-
this.showMeaning = false;
|
| 285 |
-
this.showSentence = false;
|
| 286 |
this.isCorrect = false;
|
| 287 |
this.isLoading = false;
|
| 288 |
this.fetchAudio();
|
|
@@ -327,20 +262,14 @@ export class FindwordComponent {
|
|
| 327 |
this.isPopupVisible = false;
|
| 328 |
}
|
| 329 |
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
// Meaning button logic
|
| 333 |
get isMeaningButtonDisabled(): boolean {
|
| 334 |
return !(this.isCorrect || this.attemptsLeft <= 2);
|
| 335 |
}
|
| 336 |
|
| 337 |
-
// Example button logic
|
| 338 |
get isExampleButtonDisabled(): boolean {
|
| 339 |
return !(this.isCorrect || this.attemptsLeft === 0);
|
| 340 |
}
|
| 341 |
|
| 342 |
-
|
| 343 |
-
// 12 bars looks lively without being noisy for kids
|
| 344 |
bars = Array.from({ length: 12 });
|
| 345 |
|
| 346 |
togglePlayback(): void {
|
|
@@ -351,7 +280,6 @@ export class FindwordComponent {
|
|
| 351 |
audio.play().then(() => {
|
| 352 |
this.isPlaying = true;
|
| 353 |
}).catch(() => {
|
| 354 |
-
// Autoplay guard
|
| 355 |
this.isPlaying = false;
|
| 356 |
});
|
| 357 |
} else {
|
|
@@ -367,7 +295,6 @@ export class FindwordComponent {
|
|
| 367 |
this.currentTimeDisplay = '0:00';
|
| 368 |
this.progress = 0;
|
| 369 |
this.isPlaying = false;
|
| 370 |
-
// Optional: lock typing until finished, if your flow needs it
|
| 371 |
if (typeof this.audioFinished !== 'undefined') {
|
| 372 |
this.audioFinished = false;
|
| 373 |
}
|
|
@@ -385,7 +312,6 @@ export class FindwordComponent {
|
|
| 385 |
onAudioEnded(): void {
|
| 386 |
this.isPlaying = false;
|
| 387 |
this.progress = 100;
|
| 388 |
-
// Enable typing only after full listen (matches your current UX)
|
| 389 |
if (typeof this.audioFinished !== 'undefined') {
|
| 390 |
this.audioFinished = true;
|
| 391 |
}
|
|
@@ -398,7 +324,6 @@ export class FindwordComponent {
|
|
| 398 |
return `${m}:${s.toString().padStart(2, '0')}`;
|
| 399 |
}
|
| 400 |
|
| 401 |
-
// Optional: if your existing fetchAudio resets the state, ensure it also:
|
| 402 |
private resetPlayerUI(): void {
|
| 403 |
this.isPlaying = false;
|
| 404 |
this.progress = 0;
|
|
@@ -406,7 +331,6 @@ export class FindwordComponent {
|
|
| 406 |
this.durationDisplay = '0:00';
|
| 407 |
}
|
| 408 |
|
| 409 |
-
|
| 410 |
ui = {
|
| 411 |
pulseOk: false,
|
| 412 |
shake: false,
|
|
@@ -416,37 +340,28 @@ export class FindwordComponent {
|
|
| 416 |
exampleText: ''
|
| 417 |
};
|
| 418 |
|
| 419 |
-
|
| 420 |
-
|
| 421 |
private resetStep2State(): void {
|
| 422 |
-
// stop & clear audio
|
| 423 |
const audio = this.audioRef?.nativeElement;
|
| 424 |
if (audio) {
|
| 425 |
try { audio.pause(); } catch { }
|
| 426 |
audio.currentTime = 0;
|
| 427 |
}
|
| 428 |
-
this.audioUrl = null;
|
| 429 |
this.isPlaying = false;
|
| 430 |
this.progress = 0;
|
| 431 |
this.currentTimeDisplay = '0:00';
|
| 432 |
this.durationDisplay = '0:00';
|
| 433 |
-
|
| 434 |
-
// quiz state
|
| 435 |
this.userInput = '';
|
| 436 |
this.validationMessage = '';
|
| 437 |
this.isCorrect = false;
|
| 438 |
this.attemptsLeft = 3;
|
| 439 |
this.canSubmit = false;
|
| 440 |
-
this.audioFinished = false;
|
| 441 |
-
|
| 442 |
-
// metadata/buttons
|
| 443 |
this.correctWord = null;
|
| 444 |
this.wordMeaning = null;
|
| 445 |
this.wordSentence = null;
|
| 446 |
-
this.isGenerateDisabled = false;
|
| 447 |
this.isLoading = false;
|
| 448 |
-
|
| 449 |
-
// meaning/example UI (if you use the `ui` object)
|
| 450 |
this.showMeaning = false;
|
| 451 |
this.showSentence = false;
|
| 452 |
if (this.ui) {
|
|
@@ -457,22 +372,17 @@ export class FindwordComponent {
|
|
| 457 |
this.ui.meaningText = '';
|
| 458 |
this.ui.exampleText = '';
|
| 459 |
}
|
| 460 |
-
|
| 461 |
-
// popup
|
| 462 |
this.isPopupVisible = false;
|
| 463 |
this.popupMessage = '';
|
| 464 |
}
|
| 465 |
|
| 466 |
closeToStart(): void {
|
| 467 |
-
// Safely stop and reset audio if present
|
| 468 |
const audio = this.audioRef?.nativeElement;
|
| 469 |
if (audio) {
|
| 470 |
try { audio.pause(); } catch { }
|
| 471 |
audio.currentTime = 0;
|
| 472 |
}
|
| 473 |
this.isPlaying = false;
|
| 474 |
-
|
| 475 |
-
// Clear key Step 2 data so the page is fresh when user comes back
|
| 476 |
this.audioUrl = null;
|
| 477 |
this.userInput = '';
|
| 478 |
this.validationMessage = '';
|
|
@@ -486,11 +396,8 @@ export class FindwordComponent {
|
|
| 486 |
this.showSentence = false;
|
| 487 |
this.isPopupVisible = false;
|
| 488 |
this.popupMessage = '';
|
| 489 |
-
|
| 490 |
-
// Go to starting page (Step 1)
|
| 491 |
this.step = 1;
|
| 492 |
}
|
| 493 |
-
|
| 494 |
}
|
| 495 |
|
| 496 |
|
|
|
|
| 1 |
import { Component, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
|
| 2 |
import { FindwordService } from '../findword/findword.service';
|
| 3 |
import { Router } from '@angular/router';
|
| 4 |
+
import { HeaderComponent } from '../shared/header/header.component';
|
| 5 |
+
import { ButtonComponent } from '../shared/button/button.component';
|
| 6 |
+
import { CommonModule } from '@angular/common';
|
| 7 |
+
import { FormsModule } from '@angular/forms';
|
| 8 |
|
| 9 |
@Component({
|
| 10 |
selector: 'app-findword',
|
| 11 |
templateUrl: './findword.component.html',
|
| 12 |
+
styleUrl: './findword.component.css',
|
| 13 |
+
standalone: true,
|
| 14 |
+
imports: [ButtonComponent, CommonModule, FormsModule, HeaderComponent]
|
| 15 |
})
|
| 16 |
export class FindwordComponent {
|
| 17 |
@ViewChild('audioPlayer', { static: false }) audioPlayer?: ElementRef<HTMLAudioElement>;
|
| 18 |
@ViewChild('audioPlayer') audioRef!: ElementRef<HTMLAudioElement>;
|
| 19 |
|
|
|
|
| 20 |
step: number = 1;
|
| 21 |
progress: number = 0;
|
| 22 |
audioUrl: string | null = null;
|
|
|
|
| 36 |
audioFinished: boolean = false;
|
| 37 |
canSubmit: boolean = false;
|
| 38 |
isPlaying: boolean = false;
|
|
|
|
|
|
|
| 39 |
currentTimeDisplay = '0:00';
|
| 40 |
durationDisplay = '0:00';
|
| 41 |
constructor(
|
|
|
|
| 44 |
private router: Router
|
| 45 |
) { }
|
| 46 |
|
|
|
|
| 47 |
fetchAudio() {
|
| 48 |
this.isLoading = true;
|
| 49 |
this.correctWord = null;
|
|
|
|
| 71 |
this.wordSentence = response.sentence;
|
| 72 |
console.log("✅ Correct Word Set:", this.correctWord);
|
| 73 |
|
|
|
|
| 74 |
this.audioUrl = this.findwordService.buildAssetUrl(response.audio_file_path);
|
|
|
|
|
|
|
| 75 |
this.cd.detectChanges();
|
| 76 |
|
| 77 |
setTimeout(() => {
|
|
|
|
| 121 |
this.isPopupVisible = true;
|
| 122 |
}
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
validateWord() {
|
| 125 |
if (this.attemptsLeft <= 0) {
|
|
|
|
| 126 |
this.showMeaning = true;
|
| 127 |
this.showSentence = true;
|
| 128 |
return;
|
|
|
|
| 150 |
if (response.status === 'success') {
|
| 151 |
this.validationMessage = "✅ Correct!";
|
| 152 |
this.isCorrect = true;
|
|
|
|
|
|
|
| 153 |
this.ui.pulseOk = true;
|
| 154 |
setTimeout(() => (this.ui.pulseOk = false), 1200);
|
|
|
|
| 155 |
this.attemptsLeft = 0;
|
| 156 |
this.popupMessage = this.validationMessage;
|
| 157 |
this.isGenerateDisabled = true;
|
|
|
|
| 167 |
this.validationMessage = `❌ Incorrect! You have ${this.attemptsLeft} attempt(s) left.`;
|
| 168 |
}
|
| 169 |
this.isCorrect = false;
|
|
|
|
|
|
|
| 170 |
this.ui.shake = true;
|
| 171 |
setTimeout(() => (this.ui.shake = false), 400);
|
| 172 |
}
|
|
|
|
| 176 |
});
|
| 177 |
}
|
| 178 |
|
|
|
|
| 179 |
onAudioReady() {
|
| 180 |
console.log("🎧 Audio is ready, trying to play...");
|
| 181 |
if (this.audioPlayer) {
|
|
|
|
| 191 |
this.audioPlayer.nativeElement.pause();
|
| 192 |
}
|
| 193 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
playAudio() {
|
| 195 |
if (this.audioPlayer?.nativeElement) {
|
| 196 |
this.audioPlayer.nativeElement.play();
|
|
|
|
| 216 |
this.validationMessage = '';
|
| 217 |
this.attemptsLeft = 3;
|
| 218 |
this.correctWord = null;
|
| 219 |
+
this.showMeaning = false;
|
| 220 |
+
this.showSentence = false;
|
| 221 |
this.isCorrect = false;
|
| 222 |
this.isLoading = false;
|
| 223 |
this.fetchAudio();
|
|
|
|
| 262 |
this.isPopupVisible = false;
|
| 263 |
}
|
| 264 |
|
|
|
|
|
|
|
|
|
|
| 265 |
get isMeaningButtonDisabled(): boolean {
|
| 266 |
return !(this.isCorrect || this.attemptsLeft <= 2);
|
| 267 |
}
|
| 268 |
|
|
|
|
| 269 |
get isExampleButtonDisabled(): boolean {
|
| 270 |
return !(this.isCorrect || this.attemptsLeft === 0);
|
| 271 |
}
|
| 272 |
|
|
|
|
|
|
|
| 273 |
bars = Array.from({ length: 12 });
|
| 274 |
|
| 275 |
togglePlayback(): void {
|
|
|
|
| 280 |
audio.play().then(() => {
|
| 281 |
this.isPlaying = true;
|
| 282 |
}).catch(() => {
|
|
|
|
| 283 |
this.isPlaying = false;
|
| 284 |
});
|
| 285 |
} else {
|
|
|
|
| 295 |
this.currentTimeDisplay = '0:00';
|
| 296 |
this.progress = 0;
|
| 297 |
this.isPlaying = false;
|
|
|
|
| 298 |
if (typeof this.audioFinished !== 'undefined') {
|
| 299 |
this.audioFinished = false;
|
| 300 |
}
|
|
|
|
| 312 |
onAudioEnded(): void {
|
| 313 |
this.isPlaying = false;
|
| 314 |
this.progress = 100;
|
|
|
|
| 315 |
if (typeof this.audioFinished !== 'undefined') {
|
| 316 |
this.audioFinished = true;
|
| 317 |
}
|
|
|
|
| 324 |
return `${m}:${s.toString().padStart(2, '0')}`;
|
| 325 |
}
|
| 326 |
|
|
|
|
| 327 |
private resetPlayerUI(): void {
|
| 328 |
this.isPlaying = false;
|
| 329 |
this.progress = 0;
|
|
|
|
| 331 |
this.durationDisplay = '0:00';
|
| 332 |
}
|
| 333 |
|
|
|
|
| 334 |
ui = {
|
| 335 |
pulseOk: false,
|
| 336 |
shake: false,
|
|
|
|
| 340 |
exampleText: ''
|
| 341 |
};
|
| 342 |
|
|
|
|
|
|
|
| 343 |
private resetStep2State(): void {
|
|
|
|
| 344 |
const audio = this.audioRef?.nativeElement;
|
| 345 |
if (audio) {
|
| 346 |
try { audio.pause(); } catch { }
|
| 347 |
audio.currentTime = 0;
|
| 348 |
}
|
| 349 |
+
this.audioUrl = null;
|
| 350 |
this.isPlaying = false;
|
| 351 |
this.progress = 0;
|
| 352 |
this.currentTimeDisplay = '0:00';
|
| 353 |
this.durationDisplay = '0:00';
|
|
|
|
|
|
|
| 354 |
this.userInput = '';
|
| 355 |
this.validationMessage = '';
|
| 356 |
this.isCorrect = false;
|
| 357 |
this.attemptsLeft = 3;
|
| 358 |
this.canSubmit = false;
|
| 359 |
+
this.audioFinished = false;
|
|
|
|
|
|
|
| 360 |
this.correctWord = null;
|
| 361 |
this.wordMeaning = null;
|
| 362 |
this.wordSentence = null;
|
| 363 |
+
this.isGenerateDisabled = false;
|
| 364 |
this.isLoading = false;
|
|
|
|
|
|
|
| 365 |
this.showMeaning = false;
|
| 366 |
this.showSentence = false;
|
| 367 |
if (this.ui) {
|
|
|
|
| 372 |
this.ui.meaningText = '';
|
| 373 |
this.ui.exampleText = '';
|
| 374 |
}
|
|
|
|
|
|
|
| 375 |
this.isPopupVisible = false;
|
| 376 |
this.popupMessage = '';
|
| 377 |
}
|
| 378 |
|
| 379 |
closeToStart(): void {
|
|
|
|
| 380 |
const audio = this.audioRef?.nativeElement;
|
| 381 |
if (audio) {
|
| 382 |
try { audio.pause(); } catch { }
|
| 383 |
audio.currentTime = 0;
|
| 384 |
}
|
| 385 |
this.isPlaying = false;
|
|
|
|
|
|
|
| 386 |
this.audioUrl = null;
|
| 387 |
this.userInput = '';
|
| 388 |
this.validationMessage = '';
|
|
|
|
| 396 |
this.showSentence = false;
|
| 397 |
this.isPopupVisible = false;
|
| 398 |
this.popupMessage = '';
|
|
|
|
|
|
|
| 399 |
this.step = 1;
|
| 400 |
}
|
|
|
|
| 401 |
}
|
| 402 |
|
| 403 |
|
src/app/footer/footer.component.css
CHANGED
|
@@ -21,7 +21,7 @@
|
|
| 21 |
padding: 1vw;
|
| 22 |
z-index: 2001;
|
| 23 |
overflow: visible;
|
| 24 |
-
border: 10px solid
|
| 25 |
box-sizing: border-box;
|
| 26 |
font-weight: lighter;
|
| 27 |
}
|
|
@@ -35,30 +35,10 @@
|
|
| 35 |
}
|
| 36 |
|
| 37 |
.user-guide-close-icon {
|
| 38 |
-
position: absolute;
|
| 39 |
top: -22px;
|
| 40 |
right: -22px;
|
| 41 |
-
background: #009688;
|
| 42 |
-
border: none;
|
| 43 |
-
width: 44px;
|
| 44 |
-
height: 44px;
|
| 45 |
-
border-radius: 50%;
|
| 46 |
-
display: flex;
|
| 47 |
-
align-items: center;
|
| 48 |
-
justify-content: center;
|
| 49 |
-
font-size: 2vw;
|
| 50 |
-
color: black;
|
| 51 |
-
cursor: pointer;
|
| 52 |
-
z-index: 2010;
|
| 53 |
-
box-shadow: 0 2px 8px rgba(93,145,195,0.18);
|
| 54 |
-
transition: background 0.2s, color 0.2s;
|
| 55 |
-
|
| 56 |
}
|
| 57 |
|
| 58 |
-
.user-guide-close-icon:hover {
|
| 59 |
-
background: white;
|
| 60 |
-
color: black;
|
| 61 |
-
}
|
| 62 |
|
| 63 |
.user-guide-modal li {
|
| 64 |
line-height: 1.7;
|
|
@@ -79,10 +59,10 @@
|
|
| 79 |
font-weight: 600;
|
| 80 |
}
|
| 81 |
|
| 82 |
-
.user-guide-modal a:hover {
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
}
|
| 86 |
|
| 87 |
footer {
|
| 88 |
background: linear-gradient(to right, #011022, #01030a);
|
|
@@ -223,7 +203,7 @@ footer .social-icons {
|
|
| 223 |
justify-content: center;
|
| 224 |
line-height: 1;
|
| 225 |
}
|
| 226 |
-
|
| 227 |
.footer-watermark-row {
|
| 228 |
text-align: center;
|
| 229 |
margin-top: 8px;
|
|
|
|
| 21 |
padding: 1vw;
|
| 22 |
z-index: 2001;
|
| 23 |
overflow: visible;
|
| 24 |
+
border: 10px solid var(--main-accent-color);
|
| 25 |
box-sizing: border-box;
|
| 26 |
font-weight: lighter;
|
| 27 |
}
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
.user-guide-close-icon {
|
|
|
|
| 38 |
top: -22px;
|
| 39 |
right: -22px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
}
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
.user-guide-modal li {
|
| 44 |
line-height: 1.7;
|
|
|
|
| 59 |
font-weight: 600;
|
| 60 |
}
|
| 61 |
|
| 62 |
+
.user-guide-modal a:hover {
|
| 63 |
+
color: #009688;
|
| 64 |
+
text-decoration: underline;
|
| 65 |
+
}
|
| 66 |
|
| 67 |
footer {
|
| 68 |
background: linear-gradient(to right, #011022, #01030a);
|
|
|
|
| 203 |
justify-content: center;
|
| 204 |
line-height: 1;
|
| 205 |
}
|
| 206 |
+
|
| 207 |
.footer-watermark-row {
|
| 208 |
text-align: center;
|
| 209 |
margin-top: 8px;
|
src/app/home/home.component.css
CHANGED
|
@@ -306,7 +306,7 @@ h1 {
|
|
| 306 |
padding: 1vw;
|
| 307 |
z-index: 2001;
|
| 308 |
overflow: visible;
|
| 309 |
-
border: 10px solid
|
| 310 |
box-sizing: border-box;
|
| 311 |
}
|
| 312 |
|
|
@@ -319,30 +319,10 @@ h1 {
|
|
| 319 |
}
|
| 320 |
|
| 321 |
.user-guide-close-icon {
|
| 322 |
-
position: absolute;
|
| 323 |
top: -22px;
|
| 324 |
right: -22px;
|
| 325 |
-
background: #009688;
|
| 326 |
-
border: none;
|
| 327 |
-
width: 44px;
|
| 328 |
-
height: 44px;
|
| 329 |
-
border-radius: 50%;
|
| 330 |
-
display: flex;
|
| 331 |
-
align-items: center;
|
| 332 |
-
justify-content: center;
|
| 333 |
-
font-size: 2vw;
|
| 334 |
-
color: black;
|
| 335 |
-
cursor: pointer;
|
| 336 |
-
z-index: 2010;
|
| 337 |
-
box-shadow: 0 2px 8px rgba(93,145,195,0.18);
|
| 338 |
-
transition: background 0.2s, color 0.2s;
|
| 339 |
}
|
| 340 |
|
| 341 |
-
.user-guide-close-icon:hover {
|
| 342 |
-
background: white;
|
| 343 |
-
color: black;
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
.user-guide-modal li {
|
| 347 |
line-height: 1.7;
|
| 348 |
font-size: 1.1vw;
|
|
|
|
| 306 |
padding: 1vw;
|
| 307 |
z-index: 2001;
|
| 308 |
overflow: visible;
|
| 309 |
+
border: 10px solid var(--main-accent-color);
|
| 310 |
box-sizing: border-box;
|
| 311 |
}
|
| 312 |
|
|
|
|
| 319 |
}
|
| 320 |
|
| 321 |
.user-guide-close-icon {
|
|
|
|
| 322 |
top: -22px;
|
| 323 |
right: -22px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
}
|
| 325 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
.user-guide-modal li {
|
| 327 |
line-height: 1.7;
|
| 328 |
font-size: 1.1vw;
|
src/app/listen/listen.component.css
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
|
| 2 |
body, html {
|
| 3 |
margin: 0;
|
| 4 |
padding: 0;
|
|
@@ -10,10 +9,6 @@ body, html {
|
|
| 10 |
background-color: rgb(35 34 32 / 43%);
|
| 11 |
}
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
/* Listen Card Style */
|
| 17 |
.listen-card1 {
|
| 18 |
background: rgba(255, 255, 255, 0.9);
|
| 19 |
width: 80vw;
|
|
@@ -27,14 +22,13 @@ body, html {
|
|
| 27 |
align-items: flex-start;
|
| 28 |
padding: 3vw;
|
| 29 |
gap: 2vw;
|
| 30 |
-
border: 10px solid
|
| 31 |
border-radius: 1vw;
|
| 32 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 33 |
margin: 2vw auto;
|
| 34 |
max-width: 90%;
|
| 35 |
}
|
| 36 |
|
| 37 |
-
/* Left content */
|
| 38 |
.listen-card1 .left-content {
|
| 39 |
flex: 1;
|
| 40 |
max-width: 50%;
|
|
@@ -53,7 +47,6 @@ body, html {
|
|
| 53 |
text-align: justify;
|
| 54 |
}
|
| 55 |
|
| 56 |
-
/* Right content */
|
| 57 |
.listen-card1 .video-upload-container {
|
| 58 |
flex: 1;
|
| 59 |
max-width: 50%;
|
|
@@ -70,7 +63,6 @@ body, html {
|
|
| 70 |
width: 88%;
|
| 71 |
max-width: 100%;
|
| 72 |
border-radius: 1vw;
|
| 73 |
-
/* box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.2); */
|
| 74 |
object-fit: contain;
|
| 75 |
margin-left: 4vw;
|
| 76 |
}
|
|
@@ -87,63 +79,6 @@ body, html {
|
|
| 87 |
display: none;
|
| 88 |
}
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
button,
|
| 99 |
-
.generate-btn,
|
| 100 |
-
.upload-btn,
|
| 101 |
-
.reset-btn,
|
| 102 |
-
.next-btn,
|
| 103 |
-
.prev-btn,
|
| 104 |
-
.nav-arrow,
|
| 105 |
-
.custom-file-upload,
|
| 106 |
-
.replace-btn {
|
| 107 |
-
background-color: #006780;
|
| 108 |
-
color: white;
|
| 109 |
-
border: none;
|
| 110 |
-
padding: 15px 32px;
|
| 111 |
-
font-size: 1.5vw;
|
| 112 |
-
border-radius: 0.5vw;
|
| 113 |
-
cursor: pointer;
|
| 114 |
-
transition: background-color 0.3s, transform 0.3s;
|
| 115 |
-
font-weight: bold;
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
/* Hover effect */
|
| 119 |
-
button:hover,
|
| 120 |
-
.generate-btn:hover,
|
| 121 |
-
.upload-btn:hover,
|
| 122 |
-
.reset-btn:hover,
|
| 123 |
-
.next-btn:hover,
|
| 124 |
-
.prev-btn:hover,
|
| 125 |
-
.nav-arrow:hover,
|
| 126 |
-
.custom-file-upload:hover,
|
| 127 |
-
.replace-btn:hover {
|
| 128 |
-
background-color: #18788f;
|
| 129 |
-
transform: scale(1.05);
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
/* Keep gray for normal disabled buttons */
|
| 133 |
-
button:disabled:not(.correct):not(.incorrect),
|
| 134 |
-
.generate-btn:disabled,
|
| 135 |
-
.upload-btn:disabled {
|
| 136 |
-
background-color: #b0b0b0 !important;
|
| 137 |
-
color: #ffffff;
|
| 138 |
-
cursor: not-allowed;
|
| 139 |
-
opacity: 0.6;
|
| 140 |
-
transform: none;
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
/* Loader Overlay */
|
| 147 |
.loader-overlay {
|
| 148 |
position: fixed;
|
| 149 |
top: 0;
|
|
@@ -158,9 +93,9 @@ button,
|
|
| 158 |
}
|
| 159 |
|
| 160 |
.loader {
|
| 161 |
-
font-size: 15px;
|
| 162 |
-
width: 1.5em;
|
| 163 |
-
height: 1.5em;
|
| 164 |
border-radius: 50%;
|
| 165 |
position: relative;
|
| 166 |
text-indent: -9999em;
|
|
@@ -202,13 +137,6 @@ button,
|
|
| 202 |
}
|
| 203 |
}
|
| 204 |
|
| 205 |
-
/*the question page css*/
|
| 206 |
-
|
| 207 |
-
.center-title {
|
| 208 |
-
text-align: center;
|
| 209 |
-
margin-bottom: 1vw;
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
.question-block {
|
| 213 |
background: #ffffff;
|
| 214 |
padding: 2vw;
|
|
@@ -217,11 +145,9 @@ button,
|
|
| 217 |
max-width: 80%;
|
| 218 |
margin: 2vw auto;
|
| 219 |
position: relative;
|
| 220 |
-
border: 10px solid
|
| 221 |
border-radius: 1vw;
|
| 222 |
-
|
| 223 |
-
box-shadow: inset 0 0 10px rgba(0, 150, 136, 0.5), /* inner glow */
|
| 224 |
-
0 0.4vw 0.8vw rgba(0, 0, 0, 0.2); /* existing outer shadow */
|
| 225 |
}
|
| 226 |
|
| 227 |
.options-container {
|
|
@@ -232,8 +158,6 @@ button,
|
|
| 232 |
align-items: center;
|
| 233 |
}
|
| 234 |
|
| 235 |
-
|
| 236 |
-
|
| 237 |
.options-container button {
|
| 238 |
background-color: #ffffff;
|
| 239 |
color: #333;
|
|
@@ -244,18 +168,16 @@ button,
|
|
| 244 |
font-weight: 600;
|
| 245 |
display: flex;
|
| 246 |
align-items: center;
|
| 247 |
-
justify-content: center;
|
| 248 |
box-shadow: 0 0.2vw 0.5vw rgba(0, 0, 0, 0.1);
|
| 249 |
-
min-height: 6vw;
|
| 250 |
-
height: 100%;
|
| 251 |
text-align: center;
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
word-break: break-word; /* break long words if needed */
|
| 255 |
width: 21vw;
|
| 256 |
}
|
| 257 |
|
| 258 |
-
|
| 259 |
.options-container button:hover {
|
| 260 |
background-color: #f0f9ff;
|
| 261 |
transform: scale(1.01);
|
|
@@ -276,8 +198,6 @@ button,
|
|
| 276 |
font-weight: bold;
|
| 277 |
}
|
| 278 |
|
| 279 |
-
|
| 280 |
-
|
| 281 |
.incorrect {
|
| 282 |
background-color: #f443367d !important;
|
| 283 |
border-color: #C62828 !important;
|
|
@@ -285,26 +205,6 @@ button,
|
|
| 285 |
font-weight: bold;
|
| 286 |
}
|
| 287 |
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
.correct-label,
|
| 291 |
-
.incorrect-label {
|
| 292 |
-
font-size: 1.5vw;
|
| 293 |
-
font-weight: bold;
|
| 294 |
-
display: inline-block;
|
| 295 |
-
display: block;
|
| 296 |
-
text-align: center;
|
| 297 |
-
width: 100%;
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
.correct-label {
|
| 301 |
-
color: #2e7d32;
|
| 302 |
-
}
|
| 303 |
-
|
| 304 |
-
.incorrect-label {
|
| 305 |
-
color: #d32f2f;
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
.question-footer {
|
| 309 |
display: flex;
|
| 310 |
justify-content: space-between;
|
|
@@ -317,13 +217,16 @@ button,
|
|
| 317 |
gap: 1vw;
|
| 318 |
}
|
| 319 |
|
| 320 |
-
|
| 321 |
.question-heading {
|
| 322 |
font-size: 2vw;
|
| 323 |
color: #006780;
|
| 324 |
margin: 0;
|
| 325 |
text-align: center;
|
| 326 |
font-weight: bold;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
}
|
| 328 |
|
| 329 |
.question-text {
|
|
@@ -333,17 +236,6 @@ button,
|
|
| 333 |
text-align: left;
|
| 334 |
}
|
| 335 |
|
| 336 |
-
|
| 337 |
-
.question-decor-image {
|
| 338 |
-
position: absolute;
|
| 339 |
-
top: 1vw;
|
| 340 |
-
right: 1vw;
|
| 341 |
-
width: 5vw;
|
| 342 |
-
height: auto;
|
| 343 |
-
/* z-index: 2;*/
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
|
| 347 |
.question-content-row {
|
| 348 |
display: flex;
|
| 349 |
justify-content: space-between;
|
|
@@ -353,16 +245,12 @@ button,
|
|
| 353 |
}
|
| 354 |
|
| 355 |
.question-side-image img {
|
| 356 |
-
width:
|
| 357 |
height: auto;
|
| 358 |
max-height: 16vw;
|
| 359 |
object-fit: contain;
|
| 360 |
}
|
| 361 |
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
/*suceesful message */
|
| 365 |
-
|
| 366 |
.upload-popup {
|
| 367 |
position: fixed;
|
| 368 |
top: 2vw;
|
|
@@ -399,16 +287,12 @@ button,
|
|
| 399 |
}
|
| 400 |
}
|
| 401 |
|
| 402 |
-
|
| 403 |
.button-group {
|
| 404 |
display: flex;
|
| 405 |
gap: 1vw;
|
| 406 |
margin-top: 1vw;
|
| 407 |
}
|
| 408 |
|
| 409 |
-
|
| 410 |
-
/*header question command close buttomn*/
|
| 411 |
-
|
| 412 |
.question-header-row {
|
| 413 |
display: flex;
|
| 414 |
justify-content: space-between;
|
|
@@ -416,27 +300,25 @@ button,
|
|
| 416 |
margin-bottom: 1vw;
|
| 417 |
}
|
| 418 |
|
| 419 |
-
.
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
font-size: 2vw;
|
| 423 |
-
color: #006780;
|
| 424 |
-
font-weight: bold;
|
| 425 |
-
margin: 0;
|
| 426 |
}
|
| 427 |
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
padding: 0.4vw 0.9vw;
|
| 433 |
-
border-radius: 50%;
|
| 434 |
-
cursor: pointer;
|
| 435 |
-
border: 1px solid black;
|
| 436 |
-
width: 3vw;
|
| 437 |
-
top: -27px;
|
| 438 |
-
left: 98.5%;
|
| 439 |
position: absolute;
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
}
|
| 442 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
body, html {
|
| 2 |
margin: 0;
|
| 3 |
padding: 0;
|
|
|
|
| 9 |
background-color: rgb(35 34 32 / 43%);
|
| 10 |
}
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
.listen-card1 {
|
| 13 |
background: rgba(255, 255, 255, 0.9);
|
| 14 |
width: 80vw;
|
|
|
|
| 22 |
align-items: flex-start;
|
| 23 |
padding: 3vw;
|
| 24 |
gap: 2vw;
|
| 25 |
+
border: 10px solid var(--main-accent-color);
|
| 26 |
border-radius: 1vw;
|
| 27 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 28 |
margin: 2vw auto;
|
| 29 |
max-width: 90%;
|
| 30 |
}
|
| 31 |
|
|
|
|
| 32 |
.listen-card1 .left-content {
|
| 33 |
flex: 1;
|
| 34 |
max-width: 50%;
|
|
|
|
| 47 |
text-align: justify;
|
| 48 |
}
|
| 49 |
|
|
|
|
| 50 |
.listen-card1 .video-upload-container {
|
| 51 |
flex: 1;
|
| 52 |
max-width: 50%;
|
|
|
|
| 63 |
width: 88%;
|
| 64 |
max-width: 100%;
|
| 65 |
border-radius: 1vw;
|
|
|
|
| 66 |
object-fit: contain;
|
| 67 |
margin-left: 4vw;
|
| 68 |
}
|
|
|
|
| 79 |
display: none;
|
| 80 |
}
|
| 81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
.loader-overlay {
|
| 83 |
position: fixed;
|
| 84 |
top: 0;
|
|
|
|
| 93 |
}
|
| 94 |
|
| 95 |
.loader {
|
| 96 |
+
font-size: 15px;
|
| 97 |
+
width: 1.5em;
|
| 98 |
+
height: 1.5em;
|
| 99 |
border-radius: 50%;
|
| 100 |
position: relative;
|
| 101 |
text-indent: -9999em;
|
|
|
|
| 137 |
}
|
| 138 |
}
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
.question-block {
|
| 141 |
background: #ffffff;
|
| 142 |
padding: 2vw;
|
|
|
|
| 145 |
max-width: 80%;
|
| 146 |
margin: 2vw auto;
|
| 147 |
position: relative;
|
| 148 |
+
border: 10px solid var(--main-accent-color);
|
| 149 |
border-radius: 1vw;
|
| 150 |
+
box-shadow: inset 0 0 10px rgba(0, 150, 136, 0.5), 0 0.4vw 0.8vw rgba(0, 0, 0, 0.2);
|
|
|
|
|
|
|
| 151 |
}
|
| 152 |
|
| 153 |
.options-container {
|
|
|
|
| 158 |
align-items: center;
|
| 159 |
}
|
| 160 |
|
|
|
|
|
|
|
| 161 |
.options-container button {
|
| 162 |
background-color: #ffffff;
|
| 163 |
color: #333;
|
|
|
|
| 168 |
font-weight: 600;
|
| 169 |
display: flex;
|
| 170 |
align-items: center;
|
| 171 |
+
justify-content: center;
|
| 172 |
box-shadow: 0 0.2vw 0.5vw rgba(0, 0, 0, 0.1);
|
| 173 |
+
min-height: 6vw;
|
| 174 |
+
height: 100%;
|
| 175 |
text-align: center;
|
| 176 |
+
white-space: normal;
|
| 177 |
+
word-break: break-word;
|
|
|
|
| 178 |
width: 21vw;
|
| 179 |
}
|
| 180 |
|
|
|
|
| 181 |
.options-container button:hover {
|
| 182 |
background-color: #f0f9ff;
|
| 183 |
transform: scale(1.01);
|
|
|
|
| 198 |
font-weight: bold;
|
| 199 |
}
|
| 200 |
|
|
|
|
|
|
|
| 201 |
.incorrect {
|
| 202 |
background-color: #f443367d !important;
|
| 203 |
border-color: #C62828 !important;
|
|
|
|
| 205 |
font-weight: bold;
|
| 206 |
}
|
| 207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
.question-footer {
|
| 209 |
display: flex;
|
| 210 |
justify-content: space-between;
|
|
|
|
| 217 |
gap: 1vw;
|
| 218 |
}
|
| 219 |
|
|
|
|
| 220 |
.question-heading {
|
| 221 |
font-size: 2vw;
|
| 222 |
color: #006780;
|
| 223 |
margin: 0;
|
| 224 |
text-align: center;
|
| 225 |
font-weight: bold;
|
| 226 |
+
flex: 1;
|
| 227 |
+
text-align: center;
|
| 228 |
+
font-weight: bold;
|
| 229 |
+
margin: 0;
|
| 230 |
}
|
| 231 |
|
| 232 |
.question-text {
|
|
|
|
| 236 |
text-align: left;
|
| 237 |
}
|
| 238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
.question-content-row {
|
| 240 |
display: flex;
|
| 241 |
justify-content: space-between;
|
|
|
|
| 245 |
}
|
| 246 |
|
| 247 |
.question-side-image img {
|
| 248 |
+
width: 34vw;
|
| 249 |
height: auto;
|
| 250 |
max-height: 16vw;
|
| 251 |
object-fit: contain;
|
| 252 |
}
|
| 253 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
.upload-popup {
|
| 255 |
position: fixed;
|
| 256 |
top: 2vw;
|
|
|
|
| 287 |
}
|
| 288 |
}
|
| 289 |
|
|
|
|
| 290 |
.button-group {
|
| 291 |
display: flex;
|
| 292 |
gap: 1vw;
|
| 293 |
margin-top: 1vw;
|
| 294 |
}
|
| 295 |
|
|
|
|
|
|
|
|
|
|
| 296 |
.question-header-row {
|
| 297 |
display: flex;
|
| 298 |
justify-content: space-between;
|
|
|
|
| 300 |
margin-bottom: 1vw;
|
| 301 |
}
|
| 302 |
|
| 303 |
+
.user-guide-close-icon {
|
| 304 |
+
top: -1.2vw;
|
| 305 |
+
right: -1.3vw;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
}
|
| 307 |
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
/*back arrow button*/
|
| 311 |
+
.back-btn {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
position: absolute;
|
| 313 |
+
top: 3vw;
|
| 314 |
+
left: 1vw;
|
| 315 |
+
transform: translateY(-50%);
|
| 316 |
+
cursor: pointer;
|
| 317 |
+
padding: 0 1rem;
|
| 318 |
+
transition: transform 0.3s;
|
| 319 |
+
width: 6vw;
|
| 320 |
}
|
| 321 |
|
| 322 |
+
.back-btn:hover {
|
| 323 |
+
transform: translateY(-50%) scale(1.1);
|
| 324 |
+
}
|
src/app/listen/listen.component.html
CHANGED
|
@@ -1,17 +1,9 @@
|
|
| 1 |
-
<!-- Loader shown while waiting -->
|
| 2 |
<div *ngIf="isLoading" class="loader-overlay">
|
| 3 |
<div class="loader"></div>
|
| 4 |
</div>
|
| 5 |
-
|
| 6 |
<app-header [title]="'Listen'"></app-header>
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
<!-- Background image (optional) -->
|
| 10 |
<img src="assets/images/listen.png" alt="Chat Background" class="grammar-bg" />
|
| 11 |
-
|
| 12 |
-
<!-- Card Layout for Listen Exercise -->
|
| 13 |
<div class="listen-card1" *ngIf="questions.length === 0">
|
| 14 |
-
<!-- Left Section: Description -->
|
| 15 |
<div class="left-content">
|
| 16 |
<h2>Welcome to the Listening Exercise</h2>
|
| 17 |
<p>
|
|
@@ -20,142 +12,108 @@
|
|
| 20 |
<strong>Generate Questions</strong> button will be enabled.
|
| 21 |
Answer questions to practice and strengthen understanding and memory.
|
| 22 |
</p>
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
[disabled]="!isGenerateEnabled"
|
| 26 |
-
(click)="generateQuestions()">
|
| 27 |
Generate Questions
|
| 28 |
-
</button>
|
| 29 |
</div>
|
| 30 |
-
|
| 31 |
-
<!-- Right Section: Video Upload -->
|
| 32 |
<div class="video-upload-container">
|
| 33 |
-
<!-- Show placeholder image until video is uploaded -->
|
| 34 |
<div class="video-preview-container" *ngIf="!videoUrl">
|
| 35 |
<img src="assets/images/listen/listen3.png"
|
| 36 |
alt="Upload a video"
|
| 37 |
class="video-placeholder" />
|
| 38 |
</div>
|
| 39 |
-
|
| 40 |
-
<!-- Video preview after upload -->
|
| 41 |
<div class="video-preview-container" *ngIf="videoUrl">
|
| 42 |
<video id="videoPreview"
|
| 43 |
controls
|
| 44 |
(ended)="onVideoEnded()"
|
| 45 |
controlsList="nodownload"
|
| 46 |
(error)="onVideoError()">
|
| 47 |
-
<source [src]="videoUrl"
|
|
|
|
| 48 |
Your browser does not support the video tag.
|
| 49 |
</video>
|
| 50 |
</div>
|
| 51 |
-
|
| 52 |
-
<!-- Upload input -->
|
| 53 |
<input #uploadInput
|
| 54 |
type="file"
|
| 55 |
accept="video/mp4"
|
| 56 |
(change)="onFileSelected($event)"
|
| 57 |
class="video-upload-input" />
|
| 58 |
-
|
| 59 |
-
<!-- Buttons -->
|
| 60 |
<div class="button-group">
|
| 61 |
-
<button *ngIf="videoUrl"
|
| 62 |
-
|
| 63 |
-
(click)="toggleVideoPlayPause()">
|
| 64 |
{{ isVideoPlaying ? 'Pause' : 'Play' }}
|
| 65 |
-
</button>
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
(click)="stopVideoIfPlaying(); uploadInput.click()"
|
| 69 |
-
[disabled]="isReplaceDisabled">
|
| 70 |
{{ uploadSuccess ? 'Replace Video' : 'Upload Video' }}
|
| 71 |
-
</button>
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
class="reset-btn"
|
| 75 |
-
(click)="goToListen()">
|
| 76 |
Reset
|
| 77 |
-
</button>
|
| 78 |
</div>
|
| 79 |
</div>
|
| 80 |
</div>
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
<div *ngIf="questions.length > 0" class="question-navigation">
|
| 86 |
-
|
| 87 |
<div class="question-block">
|
| 88 |
-
|
| 89 |
<div class="question-header-row">
|
| 90 |
-
<
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
</div>
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
<div class="question-content-row">
|
| 99 |
-
<!-- Left: Options -->
|
| 100 |
<div class="options-container">
|
| 101 |
<button *ngFor="let option of questions[currentQuestionIndex].options; let i = index"
|
| 102 |
(click)="checkAnswerTemp(i)"
|
| 103 |
[ngClass]="{
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
[disabled]="validated">
|
| 109 |
{{ option }}
|
| 110 |
</button>
|
| 111 |
</div>
|
| 112 |
-
|
| 113 |
-
<!-- Right: Decorative Image -->
|
| 114 |
<div class="question-side-image">
|
| 115 |
<img [src]="getFeedbackImage()" alt="Rabbit Feedback Image" />
|
| 116 |
</div>
|
| 117 |
</div>
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
<div class="question-footer">
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
(click)="prevQuestion()"
|
| 125 |
-
[disabled]="currentQuestionIndex === 0">
|
| 126 |
◀ Previous
|
| 127 |
-
</button>
|
| 128 |
-
|
| 129 |
-
<!-- Right Side Buttons -->
|
| 130 |
<div class="right-buttons">
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
(click)="validateAnswer(questions[currentQuestionIndex].answer, currentQuestionIndex)"
|
| 135 |
-
[disabled]="selectedOptionIndex === null">
|
| 136 |
Submit
|
| 137 |
-
</button>
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
<button *ngIf="validated && currentQuestionIndex < questions.length - 1"
|
| 141 |
-
class="next-btn"
|
| 142 |
-
(click)="nextQuestion()">
|
| 143 |
Next ▶
|
| 144 |
-
</button>
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
<button *ngIf="validated && currentQuestionIndex === questions.length - 1"
|
| 148 |
-
class="reset-btn"
|
| 149 |
-
(click)="resetQuestions()">
|
| 150 |
Regenerate Questions
|
| 151 |
-
</button>
|
| 152 |
</div>
|
| 153 |
</div>
|
| 154 |
</div>
|
| 155 |
</div>
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
<!-- Top-Right Popup Notification -->
|
| 159 |
<div *ngIf="popupMessage" class="upload-popup">
|
| 160 |
{{ popupMessage }}
|
| 161 |
</div>
|
|
|
|
|
|
|
| 1 |
<div *ngIf="isLoading" class="loader-overlay">
|
| 2 |
<div class="loader"></div>
|
| 3 |
</div>
|
|
|
|
| 4 |
<app-header [title]="'Listen'"></app-header>
|
|
|
|
|
|
|
|
|
|
| 5 |
<img src="assets/images/listen.png" alt="Chat Background" class="grammar-bg" />
|
|
|
|
|
|
|
| 6 |
<div class="listen-card1" *ngIf="questions.length === 0">
|
|
|
|
| 7 |
<div class="left-content">
|
| 8 |
<h2>Welcome to the Listening Exercise</h2>
|
| 9 |
<p>
|
|
|
|
| 12 |
<strong>Generate Questions</strong> button will be enabled.
|
| 13 |
Answer questions to practice and strengthen understanding and memory.
|
| 14 |
</p>
|
| 15 |
+
<app-button [disabled]="!isGenerateEnabled"
|
| 16 |
+
(click)="generateQuestions()">
|
|
|
|
|
|
|
| 17 |
Generate Questions
|
| 18 |
+
</app-button>
|
| 19 |
</div>
|
|
|
|
|
|
|
| 20 |
<div class="video-upload-container">
|
|
|
|
| 21 |
<div class="video-preview-container" *ngIf="!videoUrl">
|
| 22 |
<img src="assets/images/listen/listen3.png"
|
| 23 |
alt="Upload a video"
|
| 24 |
class="video-placeholder" />
|
| 25 |
</div>
|
|
|
|
|
|
|
| 26 |
<div class="video-preview-container" *ngIf="videoUrl">
|
| 27 |
<video id="videoPreview"
|
| 28 |
controls
|
| 29 |
(ended)="onVideoEnded()"
|
| 30 |
controlsList="nodownload"
|
| 31 |
(error)="onVideoError()">
|
| 32 |
+
<source [src]="videoUrl"
|
| 33 |
+
[type]="getVideoMimeType(videoUrl)" />
|
| 34 |
Your browser does not support the video tag.
|
| 35 |
</video>
|
| 36 |
</div>
|
|
|
|
|
|
|
| 37 |
<input #uploadInput
|
| 38 |
type="file"
|
| 39 |
accept="video/mp4"
|
| 40 |
(change)="onFileSelected($event)"
|
| 41 |
class="video-upload-input" />
|
|
|
|
|
|
|
| 42 |
<div class="button-group">
|
| 43 |
+
<app-button *ngIf="videoUrl"
|
| 44 |
+
(click)="toggleVideoPlayPause()">
|
|
|
|
| 45 |
{{ isVideoPlaying ? 'Pause' : 'Play' }}
|
| 46 |
+
</app-button>
|
| 47 |
+
<app-button (click)="stopVideoIfPlaying(); uploadInput.click()"
|
| 48 |
+
[disabled]="isReplaceDisabled">
|
|
|
|
|
|
|
| 49 |
{{ uploadSuccess ? 'Replace Video' : 'Upload Video' }}
|
| 50 |
+
</app-button>
|
| 51 |
+
<app-button *ngIf="videoUrl && isReplaceDisabled"
|
| 52 |
+
(click)="goToListen()">
|
|
|
|
|
|
|
| 53 |
Reset
|
| 54 |
+
</app-button>
|
| 55 |
</div>
|
| 56 |
</div>
|
| 57 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
<div *ngIf="questions.length > 0" class="question-navigation">
|
|
|
|
| 59 |
<div class="question-block">
|
|
|
|
| 60 |
<div class="question-header-row">
|
| 61 |
+
<img src="assets/images/back.png"
|
| 62 |
+
alt="Go Back"
|
| 63 |
+
class="back-btn"
|
| 64 |
+
(click)="goBack()" />
|
| 65 |
+
<h2 class="question-heading">
|
| 66 |
+
Question {{ currentQuestionIndex + 1 }} of {{ questions.length }}
|
| 67 |
+
</h2>
|
| 68 |
+
<button class="user-guide-close-icon"
|
| 69 |
+
(click)="goToListen()"
|
| 70 |
+
aria-label="Close">
|
| 71 |
+
×
|
| 72 |
+
</button>
|
| 73 |
</div>
|
| 74 |
+
<p class="question-text">
|
| 75 |
+
{{ questions[currentQuestionIndex].question }}
|
| 76 |
+
</p>
|
|
|
|
| 77 |
<div class="question-content-row">
|
|
|
|
| 78 |
<div class="options-container">
|
| 79 |
<button *ngFor="let option of questions[currentQuestionIndex].options; let i = index"
|
| 80 |
(click)="checkAnswerTemp(i)"
|
| 81 |
[ngClass]="{
|
| 82 |
+
'selected-option': selectedOptionIndex === i && !validated,
|
| 83 |
+
'correct': validated && option === questions[currentQuestionIndex].answer,
|
| 84 |
+
'incorrect': validated && selectedOptionIndex === i && option !== questions[currentQuestionIndex].answer
|
| 85 |
+
}"
|
| 86 |
[disabled]="validated">
|
| 87 |
{{ option }}
|
| 88 |
</button>
|
| 89 |
</div>
|
|
|
|
|
|
|
| 90 |
<div class="question-side-image">
|
| 91 |
<img [src]="getFeedbackImage()" alt="Rabbit Feedback Image" />
|
| 92 |
</div>
|
| 93 |
</div>
|
|
|
|
|
|
|
|
|
|
| 94 |
<div class="question-footer">
|
| 95 |
+
<app-button (click)="prevQuestion()"
|
| 96 |
+
[disabled]="currentQuestionIndex === 0">
|
|
|
|
|
|
|
| 97 |
◀ Previous
|
| 98 |
+
</app-button>
|
|
|
|
|
|
|
| 99 |
<div class="right-buttons">
|
| 100 |
+
<app-button *ngIf="!validated"
|
| 101 |
+
(click)="validateAnswer(questions[currentQuestionIndex].answer, currentQuestionIndex)"
|
| 102 |
+
[disabled]="selectedOptionIndex === null">
|
|
|
|
|
|
|
| 103 |
Submit
|
| 104 |
+
</app-button>
|
| 105 |
+
<app-button *ngIf="validated && currentQuestionIndex < questions.length - 1"
|
| 106 |
+
(click)="nextQuestion()">
|
|
|
|
|
|
|
|
|
|
| 107 |
Next ▶
|
| 108 |
+
</app-button>
|
| 109 |
+
<app-button *ngIf="validated && currentQuestionIndex === questions.length - 1"
|
| 110 |
+
(click)="resetQuestions()">
|
|
|
|
|
|
|
|
|
|
| 111 |
Regenerate Questions
|
| 112 |
+
</app-button>
|
| 113 |
</div>
|
| 114 |
</div>
|
| 115 |
</div>
|
| 116 |
</div>
|
|
|
|
|
|
|
|
|
|
| 117 |
<div *ngIf="popupMessage" class="upload-popup">
|
| 118 |
{{ popupMessage }}
|
| 119 |
</div>
|
src/app/listen/listen.component.ts
CHANGED
|
@@ -2,9 +2,14 @@ import { Component, OnInit } from '@angular/core';
|
|
| 2 |
import { HttpClient } from '@angular/common/http';
|
| 3 |
import { ListenService } from './listen.service';
|
| 4 |
import { Router } from '@angular/router';
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
@Component({
|
| 7 |
selector: 'app-listen',
|
|
|
|
|
|
|
| 8 |
templateUrl: './listen.component.html',
|
| 9 |
styleUrls: ['./listen.component.css']
|
| 10 |
})
|
|
@@ -27,10 +32,8 @@ export class ListenComponent implements OnInit {
|
|
| 27 |
questionsDisplayed: boolean = false;
|
| 28 |
|
| 29 |
isVideoPlaying: boolean = false;
|
| 30 |
-
|
| 31 |
isReplaceDisabled: boolean = false;
|
| 32 |
|
| 33 |
-
|
| 34 |
constructor(
|
| 35 |
private http: HttpClient,
|
| 36 |
private listenService: ListenService,
|
|
@@ -50,77 +53,40 @@ export class ListenComponent implements OnInit {
|
|
| 50 |
}
|
| 51 |
}
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
// this.uploadSuccess = true;
|
| 64 |
-
// this.isGenerateEnabled = false;
|
| 65 |
-
// // Show popup after a short delay
|
| 66 |
-
// setTimeout(() => {
|
| 67 |
-
// this.popupMessage = '✅ Video uploaded successfully!';
|
| 68 |
-
// setTimeout(() => this.popupMessage = '', 1000); // Hide after 3s
|
| 69 |
-
// }, 1000);
|
| 70 |
-
// },
|
| 71 |
-
// error: (err) => {
|
| 72 |
-
// console.error('Upload failed', err);
|
| 73 |
-
// alert('❌ Video upload failed.');
|
| 74 |
-
// }
|
| 75 |
-
// });
|
| 76 |
-
// } else {
|
| 77 |
-
// alert('Please upload a valid MP4 file.');
|
| 78 |
-
// }
|
| 79 |
-
//}
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
onFileSelected(event: any): void {
|
| 83 |
-
const file = event.target.files[0];
|
| 84 |
-
if (file && file.name.toLowerCase().endsWith('.mp4')) {
|
| 85 |
-
this.selectedFile = file;
|
| 86 |
-
const formData = new FormData();
|
| 87 |
-
formData.append('video', file);
|
| 88 |
-
|
| 89 |
-
this.listenService.uploadVideo(formData).subscribe({
|
| 90 |
-
next: (response) => {
|
| 91 |
-
// Use backend /media/videos/<filename> path for preview (works both local & HF)
|
| 92 |
-
const newVideoUrl = this.listenService.getVideoUrl(response.filename);
|
| 93 |
-
|
| 94 |
-
this.videoUrl = ''; // Reset first to force re-binding
|
| 95 |
-
setTimeout(() => {
|
| 96 |
-
this.videoUrl = newVideoUrl;
|
| 97 |
-
|
| 98 |
-
const videoElement = document.getElementById('videoPreview') as HTMLVideoElement;
|
| 99 |
-
if (videoElement) {
|
| 100 |
-
videoElement.load(); // Reload the video player
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
this.uploadSuccess = true;
|
| 104 |
-
this.isGenerateEnabled = false;
|
| 105 |
-
this.isVideoPlaying = false;
|
| 106 |
-
|
| 107 |
setTimeout(() => {
|
| 108 |
-
this.
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
}
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
|
| 124 |
|
| 125 |
onVideoEnded(): void {
|
| 126 |
this.isGenerateEnabled = true;
|
|
@@ -128,23 +94,18 @@ onFileSelected(event: any): void {
|
|
| 128 |
}
|
| 129 |
|
| 130 |
generateQuestions(): void {
|
| 131 |
-
|
| 132 |
-
this.stopVideoIfPlaying(); // stop video first
|
| 133 |
-
|
| 134 |
if (!this.videoUrl) {
|
| 135 |
-
alert(
|
| 136 |
return;
|
| 137 |
}
|
| 138 |
-
|
| 139 |
const filename = this.videoUrl.split('/').pop()?.trim();
|
| 140 |
if (!filename) {
|
| 141 |
-
alert(
|
| 142 |
-
console.error(
|
| 143 |
return;
|
| 144 |
}
|
| 145 |
-
|
| 146 |
this.isLoading = true;
|
| 147 |
-
|
| 148 |
this.listenService.generateQuestions(filename).subscribe({
|
| 149 |
next: (response) => {
|
| 150 |
if (response.questions && response.questions.length > 0) {
|
|
@@ -158,7 +119,7 @@ onFileSelected(event: any): void {
|
|
| 158 |
console.error('Error generating questions:', error);
|
| 159 |
alert(error.error?.error || 'Failed to generate questions.');
|
| 160 |
this.isLoading = false;
|
| 161 |
-
}
|
| 162 |
});
|
| 163 |
}
|
| 164 |
|
|
@@ -171,18 +132,14 @@ onFileSelected(event: any): void {
|
|
| 171 |
this.selectedOptionIndex = index;
|
| 172 |
}
|
| 173 |
|
| 174 |
-
|
| 175 |
validateAnswer(correctAnswer: string, index: number) {
|
| 176 |
if (this.selectedOptionIndex === null) return;
|
| 177 |
-
|
| 178 |
this.validated = true;
|
| 179 |
const selectedOption = this.questions[index].options[this.selectedOptionIndex];
|
| 180 |
this.selectedOptions[index] = selectedOption;
|
| 181 |
this.selectedAnswers[index] = selectedOption === correctAnswer ? 'correct' : 'incorrect';
|
| 182 |
}
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
| 186 |
prevQuestion() {
|
| 187 |
if (this.currentQuestionIndex > 0) {
|
| 188 |
this.currentQuestionIndex--;
|
|
@@ -192,7 +149,6 @@ onFileSelected(event: any): void {
|
|
| 192 |
}
|
| 193 |
}
|
| 194 |
|
| 195 |
-
|
| 196 |
nextQuestion() {
|
| 197 |
if (this.currentQuestionIndex < this.questions.length - 1) {
|
| 198 |
this.currentQuestionIndex++;
|
|
@@ -202,18 +158,6 @@ onFileSelected(event: any): void {
|
|
| 202 |
}
|
| 203 |
}
|
| 204 |
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
//resetQuestions(): void {
|
| 209 |
-
// this.questions = [];
|
| 210 |
-
// this.selectedAnswers = [];
|
| 211 |
-
// this.questionsDisplayed = false;
|
| 212 |
-
// this.currentQuestionIndex = 0; // reset to first
|
| 213 |
-
// this.generateQuestions();
|
| 214 |
-
//}
|
| 215 |
-
|
| 216 |
-
|
| 217 |
resetQuestions(): void {
|
| 218 |
this.questions = [];
|
| 219 |
this.selectedAnswers = [];
|
|
@@ -222,15 +166,12 @@ onFileSelected(event: any): void {
|
|
| 222 |
this.selectedOptionIndex = null;
|
| 223 |
this.currentQuestionIndex = 0;
|
| 224 |
this.questionsDisplayed = false;
|
| 225 |
-
|
| 226 |
const filename = this.videoUrl.split('/').pop()?.trim();
|
| 227 |
if (!filename) {
|
| 228 |
-
alert(
|
| 229 |
return;
|
| 230 |
}
|
| 231 |
-
|
| 232 |
this.isLoading = true;
|
| 233 |
-
|
| 234 |
this.listenService.generateQuestions(filename).subscribe({
|
| 235 |
next: (response) => {
|
| 236 |
if (response.questions && response.questions.length > 0) {
|
|
@@ -244,11 +185,10 @@ onFileSelected(event: any): void {
|
|
| 244 |
console.error('Error regenerating questions:', error);
|
| 245 |
alert(error.error?.error || 'Failed to regenerate questions.');
|
| 246 |
this.isLoading = false;
|
| 247 |
-
}
|
| 248 |
});
|
| 249 |
}
|
| 250 |
|
| 251 |
-
|
| 252 |
goBack(): void {
|
| 253 |
this.questions = [];
|
| 254 |
this.selectedAnswers = [];
|
|
@@ -259,10 +199,7 @@ onFileSelected(event: any): void {
|
|
| 259 |
this.router.navigate(['/home']);
|
| 260 |
}
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
goToListen(): void {
|
| 265 |
-
// Clear all state
|
| 266 |
this.questions = [];
|
| 267 |
this.selectedAnswers = [];
|
| 268 |
this.selectedOptions = {};
|
|
@@ -270,22 +207,17 @@ onFileSelected(event: any): void {
|
|
| 270 |
this.selectedOptionIndex = null;
|
| 271 |
this.currentQuestionIndex = 0;
|
| 272 |
this.questionsDisplayed = false;
|
| 273 |
-
|
| 274 |
-
// Reset video to trigger image placeholder
|
| 275 |
this.videoUrl = '';
|
| 276 |
this.uploadSuccess = false;
|
| 277 |
this.isGenerateEnabled = false;
|
| 278 |
this.isVideoPlaying = false;
|
| 279 |
this.isReplaceDisabled = false;
|
| 280 |
-
|
| 281 |
-
// Clear the file input
|
| 282 |
const uploadInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
| 283 |
if (uploadInput) {
|
| 284 |
-
uploadInput.value = '';
|
| 285 |
}
|
| 286 |
}
|
| 287 |
|
| 288 |
-
|
| 289 |
getVideoMimeType(videoUrl: string): string {
|
| 290 |
if (videoUrl.endsWith('.mp4')) return 'video/mp4';
|
| 291 |
if (videoUrl.endsWith('.mov')) return 'video/quicktime';
|
|
@@ -307,41 +239,24 @@ onFileSelected(event: any): void {
|
|
| 307 |
|
| 308 |
getFeedbackImage(): string {
|
| 309 |
if (!this.validated) {
|
| 310 |
-
return 'assets/images/listen/group.png';
|
| 311 |
}
|
| 312 |
-
|
| 313 |
const result = this.selectedAnswers[this.currentQuestionIndex];
|
| 314 |
if (result === 'correct') {
|
| 315 |
-
return 'assets/images/listen/confetti.png';
|
| 316 |
} else if (result === 'incorrect') {
|
| 317 |
-
return 'assets/images/listen/sad.png';
|
| 318 |
}
|
| 319 |
-
|
| 320 |
-
return 'assets/images/listen/group.png'; // Fallback
|
| 321 |
}
|
| 322 |
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
//toggleVideoPlayPause(): void {
|
| 326 |
-
// const videoElement = document.getElementById('videoPreview') as HTMLVideoElement;
|
| 327 |
-
// if (videoElement) {
|
| 328 |
-
// if (videoElement.paused) {
|
| 329 |
-
// videoElement.play();
|
| 330 |
-
// this.isVideoPlaying = true;
|
| 331 |
-
// } else {
|
| 332 |
-
// videoElement.pause();
|
| 333 |
-
// this.isVideoPlaying = false;
|
| 334 |
-
// }
|
| 335 |
-
// }
|
| 336 |
-
//}
|
| 337 |
-
|
| 338 |
toggleVideoPlayPause(): void {
|
| 339 |
const videoElement = document.getElementById('videoPreview') as HTMLVideoElement;
|
| 340 |
if (videoElement) {
|
| 341 |
if (videoElement.paused) {
|
| 342 |
videoElement.play();
|
| 343 |
this.isVideoPlaying = true;
|
| 344 |
-
this.isReplaceDisabled = true;
|
| 345 |
} else {
|
| 346 |
videoElement.pause();
|
| 347 |
this.isVideoPlaying = false;
|
|
@@ -349,8 +264,6 @@ onFileSelected(event: any): void {
|
|
| 349 |
}
|
| 350 |
}
|
| 351 |
|
| 352 |
-
|
| 353 |
-
|
| 354 |
stopVideoIfPlaying(): void {
|
| 355 |
const videoElement = document.getElementById('videoPreview') as HTMLVideoElement;
|
| 356 |
if (videoElement && !videoElement.paused) {
|
|
@@ -358,8 +271,4 @@ onFileSelected(event: any): void {
|
|
| 358 |
this.isVideoPlaying = false;
|
| 359 |
}
|
| 360 |
}
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
}
|
|
|
|
| 2 |
import { HttpClient } from '@angular/common/http';
|
| 3 |
import { ListenService } from './listen.service';
|
| 4 |
import { Router } from '@angular/router';
|
| 5 |
+
import { ButtonComponent } from '../shared/button/button.component';
|
| 6 |
+
import { HeaderComponent } from '../shared/header/header.component';
|
| 7 |
+
import { CommonModule } from '@angular/common';
|
| 8 |
|
| 9 |
@Component({
|
| 10 |
selector: 'app-listen',
|
| 11 |
+
standalone: true,
|
| 12 |
+
imports: [ButtonComponent, HeaderComponent, CommonModule],
|
| 13 |
templateUrl: './listen.component.html',
|
| 14 |
styleUrls: ['./listen.component.css']
|
| 15 |
})
|
|
|
|
| 32 |
questionsDisplayed: boolean = false;
|
| 33 |
|
| 34 |
isVideoPlaying: boolean = false;
|
|
|
|
| 35 |
isReplaceDisabled: boolean = false;
|
| 36 |
|
|
|
|
| 37 |
constructor(
|
| 38 |
private http: HttpClient,
|
| 39 |
private listenService: ListenService,
|
|
|
|
| 53 |
}
|
| 54 |
}
|
| 55 |
|
| 56 |
+
onFileSelected(event: any): void {
|
| 57 |
+
const file = event.target.files[0];
|
| 58 |
+
if (file && file.name.toLowerCase().endsWith('.mp4')) {
|
| 59 |
+
this.selectedFile = file;
|
| 60 |
+
const formData = new FormData();
|
| 61 |
+
formData.append('video', file);
|
| 62 |
+
this.listenService.uploadVideo(formData).subscribe({
|
| 63 |
+
next: (response) => {
|
| 64 |
+
const newVideoUrl = this.listenService.getVideoUrl(response.filename);
|
| 65 |
+
this.videoUrl = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
setTimeout(() => {
|
| 67 |
+
this.videoUrl = newVideoUrl;
|
| 68 |
+
const videoElement = document.getElementById('videoPreview') as HTMLVideoElement;
|
| 69 |
+
if (videoElement) {
|
| 70 |
+
videoElement.load();
|
| 71 |
+
}
|
| 72 |
+
this.uploadSuccess = true;
|
| 73 |
+
this.isGenerateEnabled = false;
|
| 74 |
+
this.isVideoPlaying = false;
|
| 75 |
+
setTimeout(() => {
|
| 76 |
+
this.popupMessage = '✅ Video uploaded successfully!';
|
| 77 |
+
setTimeout(() => (this.popupMessage = ''), 7000);
|
| 78 |
+
}, 1000);
|
| 79 |
+
});
|
| 80 |
+
},
|
| 81 |
+
error: (err) => {
|
| 82 |
+
console.error('Upload failed', err);
|
| 83 |
+
alert('❌ Video upload failed.');
|
| 84 |
+
}
|
| 85 |
+
});
|
| 86 |
+
} else {
|
| 87 |
+
alert('Please upload a valid MP4 file.');
|
| 88 |
+
}
|
| 89 |
}
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
onVideoEnded(): void {
|
| 92 |
this.isGenerateEnabled = true;
|
|
|
|
| 94 |
}
|
| 95 |
|
| 96 |
generateQuestions(): void {
|
| 97 |
+
this.stopVideoIfPlaying();
|
|
|
|
|
|
|
| 98 |
if (!this.videoUrl) {
|
| 99 |
+
alert('Please upload a video first.');
|
| 100 |
return;
|
| 101 |
}
|
|
|
|
| 102 |
const filename = this.videoUrl.split('/').pop()?.trim();
|
| 103 |
if (!filename) {
|
| 104 |
+
alert('Error: Unable to extract filename.');
|
| 105 |
+
console.error('Filename extraction failed from videoUrl:', this.videoUrl);
|
| 106 |
return;
|
| 107 |
}
|
|
|
|
| 108 |
this.isLoading = true;
|
|
|
|
| 109 |
this.listenService.generateQuestions(filename).subscribe({
|
| 110 |
next: (response) => {
|
| 111 |
if (response.questions && response.questions.length > 0) {
|
|
|
|
| 119 |
console.error('Error generating questions:', error);
|
| 120 |
alert(error.error?.error || 'Failed to generate questions.');
|
| 121 |
this.isLoading = false;
|
| 122 |
+
}
|
| 123 |
});
|
| 124 |
}
|
| 125 |
|
|
|
|
| 132 |
this.selectedOptionIndex = index;
|
| 133 |
}
|
| 134 |
|
|
|
|
| 135 |
validateAnswer(correctAnswer: string, index: number) {
|
| 136 |
if (this.selectedOptionIndex === null) return;
|
|
|
|
| 137 |
this.validated = true;
|
| 138 |
const selectedOption = this.questions[index].options[this.selectedOptionIndex];
|
| 139 |
this.selectedOptions[index] = selectedOption;
|
| 140 |
this.selectedAnswers[index] = selectedOption === correctAnswer ? 'correct' : 'incorrect';
|
| 141 |
}
|
| 142 |
|
|
|
|
|
|
|
| 143 |
prevQuestion() {
|
| 144 |
if (this.currentQuestionIndex > 0) {
|
| 145 |
this.currentQuestionIndex--;
|
|
|
|
| 149 |
}
|
| 150 |
}
|
| 151 |
|
|
|
|
| 152 |
nextQuestion() {
|
| 153 |
if (this.currentQuestionIndex < this.questions.length - 1) {
|
| 154 |
this.currentQuestionIndex++;
|
|
|
|
| 158 |
}
|
| 159 |
}
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
resetQuestions(): void {
|
| 162 |
this.questions = [];
|
| 163 |
this.selectedAnswers = [];
|
|
|
|
| 166 |
this.selectedOptionIndex = null;
|
| 167 |
this.currentQuestionIndex = 0;
|
| 168 |
this.questionsDisplayed = false;
|
|
|
|
| 169 |
const filename = this.videoUrl.split('/').pop()?.trim();
|
| 170 |
if (!filename) {
|
| 171 |
+
alert('Error: Unable to extract filename.');
|
| 172 |
return;
|
| 173 |
}
|
|
|
|
| 174 |
this.isLoading = true;
|
|
|
|
| 175 |
this.listenService.generateQuestions(filename).subscribe({
|
| 176 |
next: (response) => {
|
| 177 |
if (response.questions && response.questions.length > 0) {
|
|
|
|
| 185 |
console.error('Error regenerating questions:', error);
|
| 186 |
alert(error.error?.error || 'Failed to regenerate questions.');
|
| 187 |
this.isLoading = false;
|
| 188 |
+
}
|
| 189 |
});
|
| 190 |
}
|
| 191 |
|
|
|
|
| 192 |
goBack(): void {
|
| 193 |
this.questions = [];
|
| 194 |
this.selectedAnswers = [];
|
|
|
|
| 199 |
this.router.navigate(['/home']);
|
| 200 |
}
|
| 201 |
|
|
|
|
|
|
|
| 202 |
goToListen(): void {
|
|
|
|
| 203 |
this.questions = [];
|
| 204 |
this.selectedAnswers = [];
|
| 205 |
this.selectedOptions = {};
|
|
|
|
| 207 |
this.selectedOptionIndex = null;
|
| 208 |
this.currentQuestionIndex = 0;
|
| 209 |
this.questionsDisplayed = false;
|
|
|
|
|
|
|
| 210 |
this.videoUrl = '';
|
| 211 |
this.uploadSuccess = false;
|
| 212 |
this.isGenerateEnabled = false;
|
| 213 |
this.isVideoPlaying = false;
|
| 214 |
this.isReplaceDisabled = false;
|
|
|
|
|
|
|
| 215 |
const uploadInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
| 216 |
if (uploadInput) {
|
| 217 |
+
uploadInput.value = '';
|
| 218 |
}
|
| 219 |
}
|
| 220 |
|
|
|
|
| 221 |
getVideoMimeType(videoUrl: string): string {
|
| 222 |
if (videoUrl.endsWith('.mp4')) return 'video/mp4';
|
| 223 |
if (videoUrl.endsWith('.mov')) return 'video/quicktime';
|
|
|
|
| 239 |
|
| 240 |
getFeedbackImage(): string {
|
| 241 |
if (!this.validated) {
|
| 242 |
+
return 'assets/images/listen/group.png';
|
| 243 |
}
|
|
|
|
| 244 |
const result = this.selectedAnswers[this.currentQuestionIndex];
|
| 245 |
if (result === 'correct') {
|
| 246 |
+
return 'assets/images/listen/confetti.png';
|
| 247 |
} else if (result === 'incorrect') {
|
| 248 |
+
return 'assets/images/listen/sad.png';
|
| 249 |
}
|
| 250 |
+
return 'assets/images/listen/group.png';
|
|
|
|
| 251 |
}
|
| 252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
toggleVideoPlayPause(): void {
|
| 254 |
const videoElement = document.getElementById('videoPreview') as HTMLVideoElement;
|
| 255 |
if (videoElement) {
|
| 256 |
if (videoElement.paused) {
|
| 257 |
videoElement.play();
|
| 258 |
this.isVideoPlaying = true;
|
| 259 |
+
this.isReplaceDisabled = true;
|
| 260 |
} else {
|
| 261 |
videoElement.pause();
|
| 262 |
this.isVideoPlaying = false;
|
|
|
|
| 264 |
}
|
| 265 |
}
|
| 266 |
|
|
|
|
|
|
|
| 267 |
stopVideoIfPlaying(): void {
|
| 268 |
const videoElement = document.getElementById('videoPreview') as HTMLVideoElement;
|
| 269 |
if (videoElement && !videoElement.paused) {
|
|
|
|
| 271 |
this.isVideoPlaying = false;
|
| 272 |
}
|
| 273 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
}
|
src/app/reading/reading.component.css
CHANGED
|
@@ -1,43 +1,40 @@
|
|
| 1 |
-
/* === Container, header, background, main === */
|
| 2 |
.reading-container {
|
| 3 |
-
font-family: 'Segoe UI',sans-serif;
|
| 4 |
background-size: auto;
|
| 5 |
background-position: center;
|
| 6 |
background-attachment: fixed;
|
| 7 |
width: 100%;
|
| 8 |
-
height: 100
|
| 9 |
}
|
| 10 |
|
| 11 |
.main-container {
|
| 12 |
width: 85%;
|
| 13 |
margin: 5vh auto;
|
| 14 |
-
border: 9px solid
|
| 15 |
border-radius: 1.5vw;
|
| 16 |
background: #fff;
|
| 17 |
padding: 2vw;
|
| 18 |
box-shadow: 0 .4vw 1vw rgba(0,0,0,.1);
|
| 19 |
-
height: 76vh
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
/* ===== Intro split (match reference, minimal side margins) ===== */
|
| 25 |
.intro-section.split {
|
| 26 |
-
padding-inline: 1vw;
|
| 27 |
display: block;
|
| 28 |
}
|
| 29 |
|
| 30 |
.split-shell {
|
| 31 |
width: 100%;
|
| 32 |
-
max-width: none;
|
| 33 |
margin: 0 auto;
|
| 34 |
display: grid;
|
| 35 |
-
grid-template-columns: minmax(320px, 38%) 1fr;
|
| 36 |
align-items: center;
|
| 37 |
-
/*gap: 2.2vw;*/
|
| 38 |
}
|
| 39 |
|
| 40 |
-
/* Left image card */
|
| 41 |
.split-left {
|
| 42 |
display: grid;
|
| 43 |
place-items: center;
|
|
@@ -47,7 +44,6 @@
|
|
| 47 |
width: 60%;
|
| 48 |
}
|
| 49 |
|
| 50 |
-
/* Right content */
|
| 51 |
.hero-title {
|
| 52 |
font-size: 2.5vw;
|
| 53 |
font-weight: 800;
|
|
@@ -62,7 +58,6 @@
|
|
| 62 |
margin-bottom: 3vw;
|
| 63 |
}
|
| 64 |
|
| 65 |
-
/* Rows like the reference: label + control(s) */
|
| 66 |
.form-row {
|
| 67 |
display: flex;
|
| 68 |
align-items: center;
|
|
@@ -77,33 +72,40 @@
|
|
| 77 |
font-size: 1.3vw;
|
| 78 |
}
|
| 79 |
|
| 80 |
-
/* Inputs and selects (reuse your existing styles) */
|
| 81 |
.input-wrap, .select-wrap {
|
| 82 |
position: relative;
|
| 83 |
flex: 1;
|
| 84 |
}
|
| 85 |
|
| 86 |
-
.input-wrap input,
|
|
|
|
| 87 |
width: 100%;
|
| 88 |
-
|
|
|
|
|
|
|
| 89 |
border-radius: 12px;
|
| 90 |
border: 1px solid #cfe2e0;
|
| 91 |
background: #fff;
|
| 92 |
color: #0e3e45;
|
| 93 |
outline: 0;
|
| 94 |
transition: border-color .2s, box-shadow .2s;
|
|
|
|
| 95 |
}
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
-
/* Icons */
|
| 107 |
.icon-search, .icon-level {
|
| 108 |
position: absolute;
|
| 109 |
left: 12px;
|
|
@@ -125,7 +127,6 @@
|
|
| 125 |
font-size: 16px;
|
| 126 |
}
|
| 127 |
|
| 128 |
-
/* Level badge */
|
| 129 |
.badge {
|
| 130 |
position: absolute;
|
| 131 |
right: 10px;
|
|
@@ -157,35 +158,6 @@
|
|
| 157 |
background: #fff3e9;
|
| 158 |
}
|
| 159 |
|
| 160 |
-
/* Generate button aligned like “Get Topic” */
|
| 161 |
-
.btn-get {
|
| 162 |
-
min-width: 150px;
|
| 163 |
-
padding: 5px 16px;
|
| 164 |
-
border-radius: 12px;
|
| 165 |
-
border: 1px solid #009688;
|
| 166 |
-
background: #006780;
|
| 167 |
-
color: white;
|
| 168 |
-
font-weight: 700;
|
| 169 |
-
font-size: 1.5vw;
|
| 170 |
-
cursor: pointer;
|
| 171 |
-
transition: transform .08s, box-shadow .2s, opacity .2s;
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
.btn-get:disabled {
|
| 175 |
-
/*opacity: .6;*/
|
| 176 |
-
background-color: #bbbbbb;
|
| 177 |
-
cursor: not-allowed;
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
.btn-get:not(:disabled):hover {
|
| 181 |
-
box-shadow: 0 10px 24px rgba(0,150,136,.22);
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
.btn-get:not(:disabled):active {
|
| 185 |
-
transform: translateY(1px);
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
/* Suggestions (keep your existing styles; scoped to intro) */
|
| 189 |
.intro-section.split .suggestion-box {
|
| 190 |
position: absolute;
|
| 191 |
top: calc(100% + 8px);
|
|
@@ -214,7 +186,6 @@
|
|
| 214 |
background: #f1fbfa;
|
| 215 |
}
|
| 216 |
|
| 217 |
-
/* Clear (×) inside input */
|
| 218 |
.input-wrap.clearable {
|
| 219 |
position: relative;
|
| 220 |
}
|
|
@@ -224,35 +195,31 @@
|
|
| 224 |
}
|
| 225 |
|
| 226 |
.clear-btn {
|
| 227 |
-
position: absolute;
|
| 228 |
right: 12px;
|
| 229 |
-
top:
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
border:
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
cursor: pointer;
|
| 240 |
-
padding: 0;
|
| 241 |
font-size: 2vw;
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
}
|
| 244 |
|
| 245 |
.clear-btn:hover {
|
| 246 |
-
background:
|
| 247 |
-
color:
|
| 248 |
-
border
|
| 249 |
-
}
|
| 250 |
-
|
| 251 |
-
.clear-btn:active {
|
| 252 |
-
transform: translateY(-50%) scale(.98);
|
| 253 |
}
|
| 254 |
|
| 255 |
-
/* Responsive */
|
| 256 |
@media (max-width: 980px) {
|
| 257 |
.split-shell {
|
| 258 |
grid-template-columns: 1fr;
|
|
@@ -264,12 +231,10 @@
|
|
| 264 |
}
|
| 265 |
}
|
| 266 |
|
| 267 |
-
|
| 268 |
:root {
|
| 269 |
--passage-font: 21px;
|
| 270 |
}
|
| 271 |
|
| 272 |
-
/* Header */
|
| 273 |
.reading-head {
|
| 274 |
display: grid;
|
| 275 |
grid-template-columns: 44px 1fr auto;
|
|
@@ -292,63 +257,36 @@
|
|
| 292 |
gap: 8px;
|
| 293 |
}
|
| 294 |
|
| 295 |
-
.icon-btn {
|
| 296 |
-
width: 45px;
|
| 297 |
-
height: 45px;
|
| 298 |
-
display: grid;
|
| 299 |
-
place-items: center;
|
| 300 |
-
/* color: #eaf1ff; */
|
| 301 |
-
background: rgb(32 171 107 / 42%);
|
| 302 |
-
border: 4px solid rgba(255, 255, 255, .15);
|
| 303 |
-
border-radius: 10px;
|
| 304 |
-
cursor: pointer;
|
| 305 |
-
transition: transform .05s ease, box-shadow .15s ease, background .15s ease;
|
| 306 |
-
font-size: x-large;
|
| 307 |
-
font-weight: 800;
|
| 308 |
-
}
|
| 309 |
-
|
| 310 |
-
.icon-btn:hover {
|
| 311 |
-
box-shadow: 0 8px 18px rgba(0,0,0,.08);
|
| 312 |
background: #f7fcfb;
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
border-color
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
border-radius: 22px;
|
| 336 |
-
cursor: pointer;
|
| 337 |
-
transition: transform .05s ease, box-shadow .15s ease, background .15s ease;
|
| 338 |
-
font-size: x-large;
|
| 339 |
-
font-weight: 800;
|
| 340 |
-
top: -4vw;
|
| 341 |
-
position: relative;
|
| 342 |
-
right: -4vw;
|
| 343 |
-
}
|
| 344 |
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
}
|
| 350 |
|
| 351 |
-
/* Meta chips */
|
| 352 |
.reading-meta {
|
| 353 |
display: flex;
|
| 354 |
flex-wrap: wrap;
|
|
@@ -387,7 +325,6 @@
|
|
| 387 |
color: #b35a11;
|
| 388 |
}
|
| 389 |
|
| 390 |
-
/* Passage area */
|
| 391 |
.passage-shell {
|
| 392 |
position: relative;
|
| 393 |
border: 1px solid #e7f1f0;
|
|
@@ -395,13 +332,12 @@
|
|
| 395 |
background: #f3efef;
|
| 396 |
padding: 14px 16px;
|
| 397 |
box-shadow: inset 0 1px 0 rgba(255,255,255,.7);
|
| 398 |
-
max-height:
|
| 399 |
overflow: auto;
|
| 400 |
font-size: 1.4vw;
|
| 401 |
line-height: 1.8;
|
| 402 |
}
|
| 403 |
|
| 404 |
-
/* Soft scrollbars */
|
| 405 |
.passage-shell::-webkit-scrollbar {
|
| 406 |
width: 12px;
|
| 407 |
}
|
|
@@ -416,13 +352,11 @@
|
|
| 416 |
background: #f7fcfb;
|
| 417 |
}
|
| 418 |
|
| 419 |
-
/* Typography (A−/A+) */
|
| 420 |
.passage-text {
|
| 421 |
font-family: Georgia, "Times New Roman", serif;
|
| 422 |
font-size: var(--passage-font);
|
| 423 |
line-height: 1.5;
|
| 424 |
color: #1e3a3f;
|
| 425 |
-
/*letter-spacing: .2px;*/
|
| 426 |
text-align: left;
|
| 427 |
hyphens: auto;
|
| 428 |
white-space: pre-wrap;
|
|
@@ -447,7 +381,6 @@
|
|
| 447 |
font-weight: 700;
|
| 448 |
}
|
| 449 |
|
| 450 |
-
/* Footer buttons */
|
| 451 |
.reading-actions {
|
| 452 |
display: flex;
|
| 453 |
gap: 10px;
|
|
@@ -455,77 +388,26 @@
|
|
| 455 |
margin-top: 30px;
|
| 456 |
}
|
| 457 |
|
| 458 |
-
.btn-primary, .btn-danger {
|
| 459 |
-
min-width: 210px;
|
| 460 |
-
padding: 12px 18px;
|
| 461 |
-
border-radius: 12px;
|
| 462 |
-
font-weight: 700;
|
| 463 |
-
font-size: 1.3vw;
|
| 464 |
-
cursor: pointer;
|
| 465 |
-
border: 1px solid transparent;
|
| 466 |
-
transition: transform .06s, box-shadow .18s, opacity .18s;
|
| 467 |
-
}
|
| 468 |
-
|
| 469 |
-
.btn-primary {
|
| 470 |
-
color: #fff;
|
| 471 |
-
background: #006780;
|
| 472 |
-
border-color: #006780;
|
| 473 |
-
}
|
| 474 |
-
|
| 475 |
-
.btn-primary:hover {
|
| 476 |
-
box-shadow: 0 12px 24px rgba(0,103,128,.25);
|
| 477 |
-
}
|
| 478 |
-
|
| 479 |
-
.btn-primary:disabled {
|
| 480 |
-
opacity: .6;
|
| 481 |
-
cursor: not-allowed;
|
| 482 |
-
}
|
| 483 |
-
|
| 484 |
-
.btn-danger {
|
| 485 |
-
color: #fff;
|
| 486 |
-
background: #006780;
|
| 487 |
-
border-color: #006780;
|
| 488 |
-
}
|
| 489 |
-
|
| 490 |
-
.btn-danger:hover {
|
| 491 |
-
box-shadow: 0 12px 24px rgba(0,103,128,.25);
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
/* Back button (left) reuses .icon-btn */
|
| 495 |
.back {
|
| 496 |
grid-column: 1 / 2;
|
| 497 |
}
|
| 498 |
|
| 499 |
-
/* Responsive */
|
| 500 |
@media (max-width: 720px) {
|
| 501 |
.reading-title {
|
| 502 |
font-size: 22px;
|
| 503 |
}
|
| 504 |
-
|
| 505 |
-
.btn-primary, .btn-danger {
|
| 506 |
-
min-width: 160px;
|
| 507 |
-
}
|
| 508 |
}
|
| 509 |
|
| 510 |
-
/* Keep all MCQ UI inside the white card */
|
| 511 |
-
.main-container {
|
| 512 |
-
background: #fff;
|
| 513 |
-
overflow: visible;
|
| 514 |
-
}
|
| 515 |
-
/* you already have this */
|
| 516 |
-
|
| 517 |
-
/* ---- MCQ card ---- */
|
| 518 |
.mcq-card {
|
| 519 |
width: 100%;
|
| 520 |
-
margin: 0;
|
| 521 |
-
background: #fff;
|
| 522 |
border: 1px solid #d9ecea;
|
| 523 |
border-radius: 16px;
|
| 524 |
padding: 16px;
|
| 525 |
box-shadow: 0 10px 24px rgba(0,0,0,.06);
|
| 526 |
}
|
| 527 |
|
| 528 |
-
/* Header inside the card */
|
| 529 |
.mcq-card__header {
|
| 530 |
display: grid;
|
| 531 |
grid-template-columns: 44px 1fr 44px;
|
|
@@ -548,7 +430,6 @@
|
|
| 548 |
justify-content: flex-end;
|
| 549 |
}
|
| 550 |
|
| 551 |
-
/* Question pill */
|
| 552 |
.quiz-pill {
|
| 553 |
display: flex;
|
| 554 |
align-items: flex-start;
|
|
@@ -576,7 +457,6 @@
|
|
| 576 |
line-height: 1.6;
|
| 577 |
}
|
| 578 |
|
| 579 |
-
/* Options list */
|
| 580 |
.quiz-options {
|
| 581 |
list-style: none;
|
| 582 |
margin: 0;
|
|
@@ -592,7 +472,6 @@
|
|
| 592 |
}
|
| 593 |
}
|
| 594 |
|
| 595 |
-
/* Option pill */
|
| 596 |
.quiz-option-pill {
|
| 597 |
display: flex;
|
| 598 |
align-items: center;
|
|
@@ -620,7 +499,6 @@
|
|
| 620 |
color: #0e3e45;
|
| 621 |
}
|
| 622 |
|
| 623 |
-
/* States */
|
| 624 |
.quiz-option-pill.is-selected {
|
| 625 |
border-color: #a8d8d3;
|
| 626 |
background: #eefaf8;
|
|
@@ -636,7 +514,6 @@
|
|
| 636 |
background: #fff1ef;
|
| 637 |
}
|
| 638 |
|
| 639 |
-
/* Hide the native radio */
|
| 640 |
.visually-hidden {
|
| 641 |
position: absolute !important;
|
| 642 |
inset: auto auto auto auto !important;
|
|
@@ -647,7 +524,6 @@
|
|
| 647 |
white-space: nowrap;
|
| 648 |
}
|
| 649 |
|
| 650 |
-
/* Answer status */
|
| 651 |
.answer-status {
|
| 652 |
display: flex;
|
| 653 |
flex-wrap: wrap;
|
|
@@ -682,68 +558,17 @@
|
|
| 682 |
color: #3a5b60;
|
| 683 |
}
|
| 684 |
|
| 685 |
-
/* Footer buttons */
|
| 686 |
.mcq-card__footer {
|
| 687 |
display: flex;
|
| 688 |
gap: 10px;
|
| 689 |
justify-content: end;
|
| 690 |
margin-top: 6vw;
|
|
|
|
|
|
|
| 691 |
}
|
| 692 |
|
| 693 |
-
.submit-btn, .next-btn, .reset-btn {
|
| 694 |
-
min-width: 160px;
|
| 695 |
-
padding: 12px 16px;
|
| 696 |
-
border-radius: 12px;
|
| 697 |
-
font-weight: 700;
|
| 698 |
-
cursor: pointer;
|
| 699 |
-
/*border: 1px solid transparent;*/
|
| 700 |
-
transition: transform .06s, box-shadow .18s, opacity .18s;
|
| 701 |
-
font-size: 1.5vw;
|
| 702 |
-
}
|
| 703 |
-
|
| 704 |
-
.submit-btn {
|
| 705 |
-
background: #006780;
|
| 706 |
-
color: #fff;
|
| 707 |
-
border-color: #006780;
|
| 708 |
-
}
|
| 709 |
-
|
| 710 |
-
.submit-btn:hover {
|
| 711 |
-
box-shadow: 0 12px 24px rgba(0,103,128,.25);
|
| 712 |
-
}
|
| 713 |
-
|
| 714 |
-
.submit-btn:disabled {
|
| 715 |
-
box-shadow: none;
|
| 716 |
-
transform: none;
|
| 717 |
-
background-color: #cccccc;
|
| 718 |
-
color: #666666;
|
| 719 |
-
cursor: not-allowed;
|
| 720 |
-
}
|
| 721 |
-
|
| 722 |
-
.next-btn {
|
| 723 |
-
background: #006780;
|
| 724 |
-
color: #fff;
|
| 725 |
-
border-color: #006780;
|
| 726 |
-
}
|
| 727 |
-
|
| 728 |
-
.next-btn:hover {
|
| 729 |
-
box-shadow: 0 12px 24px rgba(0,103,128,.25);
|
| 730 |
-
}
|
| 731 |
-
|
| 732 |
-
.reset-btn {
|
| 733 |
-
background: #e5483b;
|
| 734 |
-
color: #fff;
|
| 735 |
-
border-color: #e5483b;
|
| 736 |
-
}
|
| 737 |
-
|
| 738 |
-
.reset-btn:hover {
|
| 739 |
-
box-shadow: 0 12px 24px rgba(229,72,59,.25);
|
| 740 |
-
}
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
/* Loader Overlay */
|
| 745 |
.loader-overlay {
|
| 746 |
-
position:
|
| 747 |
top: 0;
|
| 748 |
left: 0;
|
| 749 |
width: 100%;
|
|
@@ -799,8 +624,6 @@
|
|
| 799 |
}
|
| 800 |
}
|
| 801 |
|
| 802 |
-
|
| 803 |
-
|
| 804 |
.popup-overlay {
|
| 805 |
position: fixed;
|
| 806 |
top: 0;
|
|
@@ -814,7 +637,6 @@
|
|
| 814 |
z-index: 1000;
|
| 815 |
}
|
| 816 |
|
| 817 |
-
/* Card */
|
| 818 |
.popup-content {
|
| 819 |
background-color: #fff;
|
| 820 |
padding: 2vw;
|
|
@@ -825,35 +647,12 @@
|
|
| 825 |
width: 80%;
|
| 826 |
}
|
| 827 |
|
| 828 |
-
|
| 829 |
-
|
| 830 |
.popup-content p {
|
| 831 |
font-size: 1.4vw;
|
| 832 |
color: #dc3545;
|
| 833 |
margin-bottom: 1vw;
|
| 834 |
}
|
| 835 |
|
| 836 |
-
/* Close button */
|
| 837 |
-
.close-btn1 {
|
| 838 |
-
padding: 0.8vw 1.5vw;
|
| 839 |
-
font-size: 1.2vw;
|
| 840 |
-
background-color: #007bff;
|
| 841 |
-
color: #fff;
|
| 842 |
-
border: none;
|
| 843 |
-
border-radius: 0.5vw;
|
| 844 |
-
cursor: pointer;
|
| 845 |
-
transition: all 0.3s ease;
|
| 846 |
-
}
|
| 847 |
-
|
| 848 |
-
.close-btn1:hover {
|
| 849 |
-
filter: brightness(0.98);
|
| 850 |
-
}
|
| 851 |
-
|
| 852 |
-
.close-btn1:active {
|
| 853 |
-
transform: translateY(1px);
|
| 854 |
-
}
|
| 855 |
-
|
| 856 |
-
/* small scale-in animation */
|
| 857 |
@keyframes popupScale {
|
| 858 |
from {
|
| 859 |
transform: scale(.96);
|
|
@@ -866,8 +665,6 @@
|
|
| 866 |
}
|
| 867 |
}
|
| 868 |
|
| 869 |
-
|
| 870 |
-
/* Locked/disabled state for Topic input until a level is chosen */
|
| 871 |
.input-wrap.locked input {
|
| 872 |
background: #f7f9fb;
|
| 873 |
border-color: #dfe9e7;
|
|
@@ -879,16 +676,14 @@
|
|
| 879 |
opacity: .5;
|
| 880 |
}
|
| 881 |
|
| 882 |
-
/* optional: a subtle invisible scrim so nothing inside is clickable */
|
| 883 |
.input-wrap.locked::after {
|
| 884 |
content: "";
|
| 885 |
position: absolute;
|
| 886 |
inset: 0;
|
| 887 |
border-radius: 12px;
|
| 888 |
-
pointer-events: auto;
|
| 889 |
}
|
| 890 |
|
| 891 |
-
/* Helper note under the field */
|
| 892 |
.field-hint {
|
| 893 |
margin: 6px 2px 0;
|
| 894 |
font-size: 12px;
|
|
@@ -904,93 +699,25 @@
|
|
| 904 |
line-height: 1;
|
| 905 |
}
|
| 906 |
|
| 907 |
-
/* Keep space for the clear (×) button when enabled */
|
| 908 |
.input-wrap .has-clear {
|
| 909 |
padding-right: 44px;
|
| 910 |
}
|
| 911 |
|
| 912 |
|
| 913 |
-
|
| 914 |
-
.
|
| 915 |
-
width: 3vw; /* bigger button */
|
| 916 |
-
height: 3vw;
|
| 917 |
-
border-radius: 14px;
|
| 918 |
-
border: 1px solid #cfe2e0;
|
| 919 |
-
background: #fff;
|
| 920 |
-
display: grid;
|
| 921 |
-
place-items: center;
|
| 922 |
-
cursor: pointer;
|
| 923 |
-
box-shadow: 0 2px 8px rgba(0,0,0,.06);
|
| 924 |
-
transition: box-shadow .15s, transform .05s, border-color .15s, background .15s;
|
| 925 |
-
}
|
| 926 |
-
|
| 927 |
-
.icon-btn.back:hover {
|
| 928 |
-
background: #f7fcfb;
|
| 929 |
-
border-color: #b9d6d3;
|
| 930 |
-
box-shadow: 0 8px 18px rgba(0,0,0,.08);
|
| 931 |
-
}
|
| 932 |
-
|
| 933 |
-
.icon-btn.back:active {
|
| 934 |
-
transform: translateY(1px);
|
| 935 |
-
}
|
| 936 |
-
|
| 937 |
-
/* visible keyboard focus */
|
| 938 |
-
.icon-btn.back:focus-visible {
|
| 939 |
-
outline: 3px solid rgba(0,150,136,.35);
|
| 940 |
-
outline-offset: 2px;
|
| 941 |
-
}
|
| 942 |
-
|
| 943 |
-
/* arrow image inside the button */
|
| 944 |
-
.icon-btn.back .icon-img {
|
| 945 |
-
width: 3vw; /* bigger arrow */
|
| 946 |
-
height: 3vw;
|
| 947 |
-
object-fit: contain;
|
| 948 |
-
display: block;
|
| 949 |
-
transition: transform .12s, opacity .12s;
|
| 950 |
-
}
|
| 951 |
-
|
| 952 |
-
/* subtle nudge on hover */
|
| 953 |
-
.icon-btn.back:hover .icon-img {
|
| 954 |
-
transform: translateX(-1px);
|
| 955 |
-
}
|
| 956 |
-
|
| 957 |
-
/* mobile: slightly smaller to fit tight headers */
|
| 958 |
-
@media (max-width: 720px) {
|
| 959 |
-
.icon-btn.back {
|
| 960 |
-
width: 44px;
|
| 961 |
-
height: 44px;
|
| 962 |
-
border-radius: 12px;
|
| 963 |
-
}
|
| 964 |
-
|
| 965 |
-
.icon-btn.back .icon-img {
|
| 966 |
-
width: 22px;
|
| 967 |
-
height: 22px;
|
| 968 |
-
}
|
| 969 |
-
}
|
| 970 |
-
|
| 971 |
-
/* optional: if the header has a teal background */
|
| 972 |
-
.header-on-teal .icon-btn.back {
|
| 973 |
-
background: #007e74;
|
| 974 |
-
border-color: #00756c;
|
| 975 |
}
|
| 976 |
|
| 977 |
-
.header-on-teal .icon-btn.back .icon-img {
|
| 978 |
-
filter: brightness(0) invert(1); /* make arrow white */
|
| 979 |
-
}
|
| 980 |
-
|
| 981 |
|
| 982 |
.passage-text .highlight {
|
| 983 |
background-color: yellow;
|
| 984 |
}
|
| 985 |
|
| 986 |
-
|
| 987 |
-
/* ===== Congratulations modal ===== */
|
| 988 |
.congrats-overlay {
|
| 989 |
position: fixed;
|
| 990 |
inset: 0;
|
| 991 |
display: grid;
|
| 992 |
place-items: center;
|
| 993 |
-
/*background: rgba(0, 0, 0, 0.45);*/
|
| 994 |
}
|
| 995 |
|
| 996 |
.congrats-card {
|
|
@@ -1009,7 +736,7 @@
|
|
| 1009 |
position: relative;
|
| 1010 |
z-index: 1;
|
| 1011 |
box-shadow: 0 .4vw 1vw rgba(0, 0, 0, .1);
|
| 1012 |
-
border: 9px solid
|
| 1013 |
top: 3vw;
|
| 1014 |
}
|
| 1015 |
|
|
@@ -1029,7 +756,6 @@
|
|
| 1029 |
.congrats-card p {
|
| 1030 |
margin: 0 0 16px;
|
| 1031 |
color: #0e3e45;
|
| 1032 |
-
/* color: #4ca1af; */
|
| 1033 |
margin-bottom: 15px;
|
| 1034 |
font-size: 2.5vw;
|
| 1035 |
white-space: nowrap;
|
|
@@ -1038,7 +764,6 @@
|
|
| 1038 |
text-align: center;
|
| 1039 |
}
|
| 1040 |
|
| 1041 |
-
/* Score badge */
|
| 1042 |
.score-badge {
|
| 1043 |
display: inline-flex;
|
| 1044 |
align-items: baseline;
|
|
@@ -1049,7 +774,6 @@
|
|
| 1049 |
background: #eef7f6;
|
| 1050 |
color: #0e3e45;
|
| 1051 |
font-weight: 800;
|
| 1052 |
-
/* color: #4ca1af; */
|
| 1053 |
margin-bottom: 15px;
|
| 1054 |
font-size: 2.5vw;
|
| 1055 |
white-space: nowrap;
|
|
@@ -1058,21 +782,7 @@
|
|
| 1058 |
text-align: center;
|
| 1059 |
}
|
| 1060 |
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
min-width: 180px;
|
| 1065 |
-
padding: 12px 16px;
|
| 1066 |
-
border-radius: 12px;
|
| 1067 |
-
background: #006780;
|
| 1068 |
-
color: #fff;
|
| 1069 |
-
border: 1px solid #006780;
|
| 1070 |
-
font-weight: 800;
|
| 1071 |
-
cursor: pointer;
|
| 1072 |
-
transition: transform .06s, box-shadow .18s;
|
| 1073 |
-
font-size: 2vw;
|
| 1074 |
}
|
| 1075 |
-
|
| 1076 |
-
.start-over-btn:hover {
|
| 1077 |
-
box-shadow: 0 12px 24px rgba(0,103,128,.25);
|
| 1078 |
-
}
|
|
|
|
|
|
|
| 1 |
.reading-container {
|
| 2 |
+
font-family: 'Segoe UI', sans-serif;
|
| 3 |
background-size: auto;
|
| 4 |
background-position: center;
|
| 5 |
background-attachment: fixed;
|
| 6 |
width: 100%;
|
| 7 |
+
height: 100%;
|
| 8 |
}
|
| 9 |
|
| 10 |
.main-container {
|
| 11 |
width: 85%;
|
| 12 |
margin: 5vh auto;
|
| 13 |
+
border: 9px solid var(--main-accent-color);
|
| 14 |
border-radius: 1.5vw;
|
| 15 |
background: #fff;
|
| 16 |
padding: 2vw;
|
| 17 |
box-shadow: 0 .4vw 1vw rgba(0,0,0,.1);
|
| 18 |
+
height: 76vh;
|
| 19 |
+
position: relative;
|
| 20 |
+
background: #fff;
|
| 21 |
+
overflow: visible;
|
| 22 |
}
|
| 23 |
|
|
|
|
|
|
|
|
|
|
| 24 |
.intro-section.split {
|
| 25 |
+
padding-inline: 1vw;
|
| 26 |
display: block;
|
| 27 |
}
|
| 28 |
|
| 29 |
.split-shell {
|
| 30 |
width: 100%;
|
| 31 |
+
max-width: none;
|
| 32 |
margin: 0 auto;
|
| 33 |
display: grid;
|
| 34 |
+
grid-template-columns: minmax(320px, 38%) 1fr;
|
| 35 |
align-items: center;
|
|
|
|
| 36 |
}
|
| 37 |
|
|
|
|
| 38 |
.split-left {
|
| 39 |
display: grid;
|
| 40 |
place-items: center;
|
|
|
|
| 44 |
width: 60%;
|
| 45 |
}
|
| 46 |
|
|
|
|
| 47 |
.hero-title {
|
| 48 |
font-size: 2.5vw;
|
| 49 |
font-weight: 800;
|
|
|
|
| 58 |
margin-bottom: 3vw;
|
| 59 |
}
|
| 60 |
|
|
|
|
| 61 |
.form-row {
|
| 62 |
display: flex;
|
| 63 |
align-items: center;
|
|
|
|
| 72 |
font-size: 1.3vw;
|
| 73 |
}
|
| 74 |
|
|
|
|
| 75 |
.input-wrap, .select-wrap {
|
| 76 |
position: relative;
|
| 77 |
flex: 1;
|
| 78 |
}
|
| 79 |
|
| 80 |
+
.input-wrap input,
|
| 81 |
+
.select-wrap select {
|
| 82 |
width: 100%;
|
| 83 |
+
min-width: 220px;
|
| 84 |
+
font-size: 1.1rem;
|
| 85 |
+
padding: 14px 44px 14px 40px;
|
| 86 |
border-radius: 12px;
|
| 87 |
border: 1px solid #cfe2e0;
|
| 88 |
background: #fff;
|
| 89 |
color: #0e3e45;
|
| 90 |
outline: 0;
|
| 91 |
transition: border-color .2s, box-shadow .2s;
|
| 92 |
+
box-sizing: border-box;
|
| 93 |
}
|
| 94 |
|
| 95 |
+
.select-wrap select,
|
| 96 |
+
.select-wrap option {
|
| 97 |
+
font-size: 1.1rem;
|
| 98 |
+
}
|
| 99 |
|
| 100 |
+
.input-wrap input::placeholder {
|
| 101 |
+
color: #93a3b8;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.input-wrap input:focus, .select-wrap select:focus {
|
| 105 |
+
border-color: #009688;
|
| 106 |
+
box-shadow: 0 0 0 3px rgba(0,150,136,.12);
|
| 107 |
+
}
|
| 108 |
|
|
|
|
| 109 |
.icon-search, .icon-level {
|
| 110 |
position: absolute;
|
| 111 |
left: 12px;
|
|
|
|
| 127 |
font-size: 16px;
|
| 128 |
}
|
| 129 |
|
|
|
|
| 130 |
.badge {
|
| 131 |
position: absolute;
|
| 132 |
right: 10px;
|
|
|
|
| 158 |
background: #fff3e9;
|
| 159 |
}
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
.intro-section.split .suggestion-box {
|
| 162 |
position: absolute;
|
| 163 |
top: calc(100% + 8px);
|
|
|
|
| 186 |
background: #f1fbfa;
|
| 187 |
}
|
| 188 |
|
|
|
|
| 189 |
.input-wrap.clearable {
|
| 190 |
position: relative;
|
| 191 |
}
|
|
|
|
| 195 |
}
|
| 196 |
|
| 197 |
.clear-btn {
|
|
|
|
| 198 |
right: 12px;
|
| 199 |
+
top: 4px;
|
| 200 |
+
position: absolute;
|
| 201 |
+
background: #009688;
|
| 202 |
+
border: none;
|
| 203 |
+
width: 44px;
|
| 204 |
+
height: 44px;
|
| 205 |
+
border-radius: 50%;
|
| 206 |
+
display: flex;
|
| 207 |
+
align-items: center;
|
| 208 |
+
justify-content: center;
|
|
|
|
|
|
|
| 209 |
font-size: 2vw;
|
| 210 |
+
color: black;
|
| 211 |
+
cursor: pointer;
|
| 212 |
+
z-index: 2010;
|
| 213 |
+
box-shadow: 0 2px 8px rgba(93, 145, 195, 0.18);
|
| 214 |
+
transition: background 0.2s, color 0.2s;
|
| 215 |
}
|
| 216 |
|
| 217 |
.clear-btn:hover {
|
| 218 |
+
background: white;
|
| 219 |
+
color: black;
|
| 220 |
+
border: 3px solid #009688;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
}
|
| 222 |
|
|
|
|
| 223 |
@media (max-width: 980px) {
|
| 224 |
.split-shell {
|
| 225 |
grid-template-columns: 1fr;
|
|
|
|
| 231 |
}
|
| 232 |
}
|
| 233 |
|
|
|
|
| 234 |
:root {
|
| 235 |
--passage-font: 21px;
|
| 236 |
}
|
| 237 |
|
|
|
|
| 238 |
.reading-head {
|
| 239 |
display: grid;
|
| 240 |
grid-template-columns: 44px 1fr auto;
|
|
|
|
| 257 |
gap: 8px;
|
| 258 |
}
|
| 259 |
|
| 260 |
+
.head-actions .icon-btn {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
background: #f7fcfb;
|
| 262 |
+
border: 1px solid #cfe2e0;
|
| 263 |
+
border-radius: 8px;
|
| 264 |
+
padding: 0.4em 1.1em;
|
| 265 |
+
font-size: 1.2vw;
|
| 266 |
+
color: #009688;
|
| 267 |
+
font-weight: 700;
|
| 268 |
+
cursor: pointer;
|
| 269 |
+
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
| 270 |
+
outline: none;
|
| 271 |
+
min-width: 2.5em;
|
| 272 |
+
min-height: 2.5em;
|
| 273 |
+
display: flex;
|
| 274 |
+
align-items: center;
|
| 275 |
+
justify-content: center;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.head-actions .icon-btn:hover,
|
| 279 |
+
.head-actions .icon-btn.active {
|
| 280 |
+
background: #e0f7f4;
|
| 281 |
+
border-color: #009688;
|
| 282 |
+
color: #00695c;
|
| 283 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
|
| 285 |
+
.head-actions .icon-btn[aria-pressed="true"] {
|
| 286 |
+
background: #b2dfdb;
|
| 287 |
+
color: #004d40;
|
| 288 |
+
}
|
|
|
|
| 289 |
|
|
|
|
| 290 |
.reading-meta {
|
| 291 |
display: flex;
|
| 292 |
flex-wrap: wrap;
|
|
|
|
| 325 |
color: #b35a11;
|
| 326 |
}
|
| 327 |
|
|
|
|
| 328 |
.passage-shell {
|
| 329 |
position: relative;
|
| 330 |
border: 1px solid #e7f1f0;
|
|
|
|
| 332 |
background: #f3efef;
|
| 333 |
padding: 14px 16px;
|
| 334 |
box-shadow: inset 0 1px 0 rgba(255,255,255,.7);
|
| 335 |
+
max-height: 43vh;
|
| 336 |
overflow: auto;
|
| 337 |
font-size: 1.4vw;
|
| 338 |
line-height: 1.8;
|
| 339 |
}
|
| 340 |
|
|
|
|
| 341 |
.passage-shell::-webkit-scrollbar {
|
| 342 |
width: 12px;
|
| 343 |
}
|
|
|
|
| 352 |
background: #f7fcfb;
|
| 353 |
}
|
| 354 |
|
|
|
|
| 355 |
.passage-text {
|
| 356 |
font-family: Georgia, "Times New Roman", serif;
|
| 357 |
font-size: var(--passage-font);
|
| 358 |
line-height: 1.5;
|
| 359 |
color: #1e3a3f;
|
|
|
|
| 360 |
text-align: left;
|
| 361 |
hyphens: auto;
|
| 362 |
white-space: pre-wrap;
|
|
|
|
| 381 |
font-weight: 700;
|
| 382 |
}
|
| 383 |
|
|
|
|
| 384 |
.reading-actions {
|
| 385 |
display: flex;
|
| 386 |
gap: 10px;
|
|
|
|
| 388 |
margin-top: 30px;
|
| 389 |
}
|
| 390 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
.back {
|
| 392 |
grid-column: 1 / 2;
|
| 393 |
}
|
| 394 |
|
|
|
|
| 395 |
@media (max-width: 720px) {
|
| 396 |
.reading-title {
|
| 397 |
font-size: 22px;
|
| 398 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
}
|
| 400 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
.mcq-card {
|
| 402 |
width: 100%;
|
| 403 |
+
margin: 0;
|
| 404 |
+
background: #fff;
|
| 405 |
border: 1px solid #d9ecea;
|
| 406 |
border-radius: 16px;
|
| 407 |
padding: 16px;
|
| 408 |
box-shadow: 0 10px 24px rgba(0,0,0,.06);
|
| 409 |
}
|
| 410 |
|
|
|
|
| 411 |
.mcq-card__header {
|
| 412 |
display: grid;
|
| 413 |
grid-template-columns: 44px 1fr 44px;
|
|
|
|
| 430 |
justify-content: flex-end;
|
| 431 |
}
|
| 432 |
|
|
|
|
| 433 |
.quiz-pill {
|
| 434 |
display: flex;
|
| 435 |
align-items: flex-start;
|
|
|
|
| 457 |
line-height: 1.6;
|
| 458 |
}
|
| 459 |
|
|
|
|
| 460 |
.quiz-options {
|
| 461 |
list-style: none;
|
| 462 |
margin: 0;
|
|
|
|
| 472 |
}
|
| 473 |
}
|
| 474 |
|
|
|
|
| 475 |
.quiz-option-pill {
|
| 476 |
display: flex;
|
| 477 |
align-items: center;
|
|
|
|
| 499 |
color: #0e3e45;
|
| 500 |
}
|
| 501 |
|
|
|
|
| 502 |
.quiz-option-pill.is-selected {
|
| 503 |
border-color: #a8d8d3;
|
| 504 |
background: #eefaf8;
|
|
|
|
| 514 |
background: #fff1ef;
|
| 515 |
}
|
| 516 |
|
|
|
|
| 517 |
.visually-hidden {
|
| 518 |
position: absolute !important;
|
| 519 |
inset: auto auto auto auto !important;
|
|
|
|
| 524 |
white-space: nowrap;
|
| 525 |
}
|
| 526 |
|
|
|
|
| 527 |
.answer-status {
|
| 528 |
display: flex;
|
| 529 |
flex-wrap: wrap;
|
|
|
|
| 558 |
color: #3a5b60;
|
| 559 |
}
|
| 560 |
|
|
|
|
| 561 |
.mcq-card__footer {
|
| 562 |
display: flex;
|
| 563 |
gap: 10px;
|
| 564 |
justify-content: end;
|
| 565 |
margin-top: 6vw;
|
| 566 |
+
align-items: flex-end;
|
| 567 |
+
justify-content: space-between;
|
| 568 |
}
|
| 569 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
.loader-overlay {
|
| 571 |
+
position: absolute;
|
| 572 |
top: 0;
|
| 573 |
left: 0;
|
| 574 |
width: 100%;
|
|
|
|
| 624 |
}
|
| 625 |
}
|
| 626 |
|
|
|
|
|
|
|
| 627 |
.popup-overlay {
|
| 628 |
position: fixed;
|
| 629 |
top: 0;
|
|
|
|
| 637 |
z-index: 1000;
|
| 638 |
}
|
| 639 |
|
|
|
|
| 640 |
.popup-content {
|
| 641 |
background-color: #fff;
|
| 642 |
padding: 2vw;
|
|
|
|
| 647 |
width: 80%;
|
| 648 |
}
|
| 649 |
|
|
|
|
|
|
|
| 650 |
.popup-content p {
|
| 651 |
font-size: 1.4vw;
|
| 652 |
color: #dc3545;
|
| 653 |
margin-bottom: 1vw;
|
| 654 |
}
|
| 655 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
@keyframes popupScale {
|
| 657 |
from {
|
| 658 |
transform: scale(.96);
|
|
|
|
| 665 |
}
|
| 666 |
}
|
| 667 |
|
|
|
|
|
|
|
| 668 |
.input-wrap.locked input {
|
| 669 |
background: #f7f9fb;
|
| 670 |
border-color: #dfe9e7;
|
|
|
|
| 676 |
opacity: .5;
|
| 677 |
}
|
| 678 |
|
|
|
|
| 679 |
.input-wrap.locked::after {
|
| 680 |
content: "";
|
| 681 |
position: absolute;
|
| 682 |
inset: 0;
|
| 683 |
border-radius: 12px;
|
| 684 |
+
pointer-events: auto;
|
| 685 |
}
|
| 686 |
|
|
|
|
| 687 |
.field-hint {
|
| 688 |
margin: 6px 2px 0;
|
| 689 |
font-size: 12px;
|
|
|
|
| 699 |
line-height: 1;
|
| 700 |
}
|
| 701 |
|
|
|
|
| 702 |
.input-wrap .has-clear {
|
| 703 |
padding-right: 44px;
|
| 704 |
}
|
| 705 |
|
| 706 |
|
| 707 |
+
.icon-img {
|
| 708 |
+
width: 4.5vw;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
}
|
| 710 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 711 |
|
| 712 |
.passage-text .highlight {
|
| 713 |
background-color: yellow;
|
| 714 |
}
|
| 715 |
|
|
|
|
|
|
|
| 716 |
.congrats-overlay {
|
| 717 |
position: fixed;
|
| 718 |
inset: 0;
|
| 719 |
display: grid;
|
| 720 |
place-items: center;
|
|
|
|
| 721 |
}
|
| 722 |
|
| 723 |
.congrats-card {
|
|
|
|
| 736 |
position: relative;
|
| 737 |
z-index: 1;
|
| 738 |
box-shadow: 0 .4vw 1vw rgba(0, 0, 0, .1);
|
| 739 |
+
border: 9px solid var(--main-accent-color);
|
| 740 |
top: 3vw;
|
| 741 |
}
|
| 742 |
|
|
|
|
| 756 |
.congrats-card p {
|
| 757 |
margin: 0 0 16px;
|
| 758 |
color: #0e3e45;
|
|
|
|
| 759 |
margin-bottom: 15px;
|
| 760 |
font-size: 2.5vw;
|
| 761 |
white-space: nowrap;
|
|
|
|
| 764 |
text-align: center;
|
| 765 |
}
|
| 766 |
|
|
|
|
| 767 |
.score-badge {
|
| 768 |
display: inline-flex;
|
| 769 |
align-items: baseline;
|
|
|
|
| 774 |
background: #eef7f6;
|
| 775 |
color: #0e3e45;
|
| 776 |
font-weight: 800;
|
|
|
|
| 777 |
margin-bottom: 15px;
|
| 778 |
font-size: 2.5vw;
|
| 779 |
white-space: nowrap;
|
|
|
|
| 782 |
text-align: center;
|
| 783 |
}
|
| 784 |
|
| 785 |
+
.user-guide-close-icon {
|
| 786 |
+
right: -21px;
|
| 787 |
+
top: -21px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 788 |
}
|
|
|
|
|
|
|
|
|
|
|
|
src/app/reading/reading.component.html
CHANGED
|
@@ -1,89 +1,39 @@
|
|
| 1 |
<div class="reading-container">
|
| 2 |
<app-header [title]="'Reading'"></app-header>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
<!-- Background -->
|
| 6 |
<img src="assets/images/grammar-bg.png" alt="Background" class="grammar-bg" />
|
| 7 |
|
| 8 |
-
<!-- Main -->
|
| 9 |
<div class="main-container" *ngIf="!showCongrats">
|
| 10 |
-
<!-- INTRO (split left image / right form) -->
|
| 11 |
<section class="intro-section split" *ngIf="!content && !hasStarted">
|
| 12 |
<div class="split-shell">
|
| 13 |
-
<!-- Left -->
|
| 14 |
<div class="split-left">
|
| 15 |
-
<img class="intro-illustration"
|
| 16 |
-
src="assets/images/reading/teacher.png"
|
| 17 |
-
alt="Reading and quiz illustration" />
|
| 18 |
</div>
|
| 19 |
-
|
| 20 |
-
<!-- Right -->
|
| 21 |
<div class="split-right">
|
| 22 |
<h1 class="hero-title">Welcome to the Reading Exercise</h1>
|
| 23 |
<p class="hero-copy">
|
| 24 |
-
The Reading component is a simple tool that turns any meaningful topic into a short,
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
it generates multiple-choice questions and gives instant feedback. Read-Aloud and A−/A+
|
| 28 |
-
controls support different learning needs. This helps students build comprehension and
|
| 29 |
-
vocabulary, and saves teachers and parents time during practice, homework, and revision.
|
| 30 |
</p>
|
| 31 |
-
|
| 32 |
-
<!-- Row: Topic -->
|
| 33 |
<div class="form-row">
|
| 34 |
<label class="row-label">Topic:</label>
|
| 35 |
-
|
| 36 |
<div class="input-wrap clearable" [class.locked]="!difficulty">
|
| 37 |
<span class="icon-search" aria-hidden="true"></span>
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
[(ngModel)]="topic"
|
| 42 |
-
[placeholder]="difficulty ? 'Enter or select a topic' : 'Select a level first'"
|
| 43 |
-
[disabled]="!difficulty"
|
| 44 |
-
(focus)="openSuggestions()"
|
| 45 |
-
(input)="onTyping()"
|
| 46 |
-
(keydown)="onKeydown($event)"
|
| 47 |
-
(blur)="hideSuggestionsWithDelay()"
|
| 48 |
-
autocomplete="off"
|
| 49 |
-
aria-autocomplete="list"
|
| 50 |
-
[attr.aria-expanded]="showSuggestions && !!difficulty"
|
| 51 |
-
[attr.aria-disabled]="!difficulty" />
|
| 52 |
-
|
| 53 |
-
<!-- Clear (×) only when enabled and has text -->
|
| 54 |
-
<button class="clear-btn"
|
| 55 |
-
*ngIf="difficulty && topic.trim().length"
|
| 56 |
-
(mousedown)="$event.preventDefault()"
|
| 57 |
-
(click)="onClearTopic()">
|
| 58 |
-
×
|
| 59 |
-
</button>
|
| 60 |
-
|
| 61 |
-
<!-- Suggestions only after a level is chosen -->
|
| 62 |
<div class="suggestion-box" *ngIf="difficulty && showSuggestions">
|
| 63 |
<ng-container *ngIf="filteredSuggestions?.length">
|
| 64 |
-
<span *ngFor="let s of filteredSuggestions; let i = index"
|
| 65 |
-
(mousedown)="selectSuggestion(s)"
|
| 66 |
-
[class.active]="i === activeIndex">{{ s }}</span>
|
| 67 |
</ng-container>
|
| 68 |
-
<!--
|
| 69 |
-
<ng-template #noMatches>
|
| 70 |
-
<span (mousedown)="selectSuggestion(topic)">Use “{{ topic }}”</span>
|
| 71 |
-
</ng-template>
|
| 72 |
-
-->
|
| 73 |
</div>
|
| 74 |
</div>
|
| 75 |
-
|
| 76 |
-
<!-- Tiny helper note shown only when level not chosen -->
|
| 77 |
<div class="field-hint" *ngIf="!difficulty">
|
| 78 |
-
<span class="lock-icon" aria-hidden="true"></span>
|
| 79 |
-
Please select a level to enable topic suggestions.
|
| 80 |
</div>
|
| 81 |
</div>
|
| 82 |
-
|
| 83 |
-
<!-- Row: Level + Generate -->
|
| 84 |
<div class="form-row">
|
| 85 |
<label class="row-label">Select Level:</label>
|
| 86 |
-
|
| 87 |
<div class="select-wrap">
|
| 88 |
<span class="icon-level" aria-hidden="true"></span>
|
| 89 |
<select [(ngModel)]="difficulty" (ngModelChange)="onDifficultyChange($event)" required>
|
|
@@ -91,189 +41,96 @@
|
|
| 91 |
<option [ngValue]="'medium'">Medium</option>
|
| 92 |
<option [ngValue]="'hard'">Hard</option>
|
| 93 |
</select>
|
| 94 |
-
<span class="badge" *ngIf="difficulty" [attr.data-level]="difficulty">
|
| 95 |
-
{{ difficulty | titlecase }}
|
| 96 |
-
</span>
|
| 97 |
</div>
|
| 98 |
-
|
| 99 |
-
<button class="btn-get"
|
| 100 |
-
(click)="generateContent()
|
| 101 |
-
"
|
| 102 |
-
[disabled]="!topic.trim() || !difficulty.trim() || isGenerateDisabled">
|
| 103 |
-
Generate Passage
|
| 104 |
-
</button>
|
| 105 |
</div>
|
| 106 |
</div>
|
| 107 |
</div>
|
| 108 |
-
|
| 109 |
-
<!-- Centered loader (uses your .loader-overlay/.loader CSS) -->
|
| 110 |
<div class="loader-overlay" *ngIf="isGeneratingContent" role="status" aria-live="polite">
|
| 111 |
<div class="loader">Loading</div>
|
| 112 |
</div>
|
| 113 |
-
|
| 114 |
-
<!-- Error popup (uses existing CSS .popup-overlay/.popup-content) -->
|
| 115 |
<div class="popup-overlay" *ngIf="showPopup">
|
| 116 |
<div class="popup-content">
|
| 117 |
-
|
| 118 |
-
<
|
| 119 |
-
{{ errorMessage || 'We could not create the passage right now. Please try again.' }}
|
| 120 |
-
</p>
|
| 121 |
-
<button class="close-btn1" (click)="closeErrorPopup()">Close</button>
|
| 122 |
</div>
|
| 123 |
</div>
|
| 124 |
</section>
|
| 125 |
|
| 126 |
-
<!-- READING CARD -->
|
| 127 |
<div *ngIf="content && !hasStarted" class="reading-card">
|
| 128 |
-
<!-- Header -->
|
| 129 |
<div class="reading-head">
|
| 130 |
-
<
|
| 131 |
-
<img src="assets/images/reading/back.png" alt="" class="icon-img" />
|
| 132 |
-
</button>
|
| 133 |
-
|
| 134 |
<h2 class="reading-title">Let’s Start Reading!</h2>
|
| 135 |
-
|
| 136 |
-
<!-- Actions: A− A+ Read/Pause Stop -->
|
| 137 |
<div class="head-actions">
|
| 138 |
<button class="icon-btn" (click)="decreaseFont()" aria-label="Decrease font">A−</button>
|
| 139 |
<button class="icon-btn" (click)="increaseFont()" aria-label="Increase font">A+</button>
|
| 140 |
-
|
| 141 |
-
<button class="icon-btn"
|
| 142 |
-
[class.active]="isReading || ttsPaused"
|
| 143 |
-
(click)="toggleReadAloud()"
|
| 144 |
-
[attr.aria-pressed]="isReading || ttsPaused"
|
| 145 |
-
aria-label="Read aloud">
|
| 146 |
-
{{ isReading ? '⏸' : '🔊' }}
|
| 147 |
-
</button>
|
| 148 |
-
|
| 149 |
-
<!--
|
| 150 |
-
<button class="icon-btn danger" (click)="stopReadAloud()" aria-label="Stop reading">■</button>
|
| 151 |
-
-->
|
| 152 |
</div>
|
| 153 |
</div>
|
| 154 |
-
|
| 155 |
-
<!-- Meta -->
|
| 156 |
<div class="reading-meta">
|
| 157 |
<span class="chip chip-topic" *ngIf="normalizedTopic || topic">📚 {{ normalizedTopic || topic }}</span>
|
| 158 |
-
<span class="chip chip-level" [attr.data-level]="difficulty">
|
| 159 |
-
{{
|
| 160 |
-
difficulty | titlecase
|
| 161 |
-
}}
|
| 162 |
-
</span>
|
| 163 |
</div>
|
| 164 |
-
|
| 165 |
-
<!-- Passage -->
|
| 166 |
<div class="passage-shell">
|
| 167 |
<div class="passage-text" [innerHTML]="transformContent(content)"></div>
|
| 168 |
</div>
|
| 169 |
-
|
| 170 |
-
<!-- Footer -->
|
| 171 |
<div class="reading-actions">
|
| 172 |
-
<button
|
| 173 |
-
(click)="stopReadAloud(); generateQuestions()"
|
| 174 |
-
[disabled]="isGenerateQuestionDisabled">
|
| 175 |
-
Generate Questions
|
| 176 |
-
</button>
|
| 177 |
-
|
| 178 |
-
<!-- Centered loader while generating questions -->
|
| 179 |
<div class="loader-overlay" *ngIf="loadingQuestions" aria-live="polite" aria-busy="true">
|
| 180 |
<span class="loader">Loading</span>
|
| 181 |
</div>
|
| 182 |
-
|
| 183 |
-
<button class="btn-danger" (click)="stopReadAloud(); resetAll()">Reset</button>
|
| 184 |
</div>
|
| 185 |
</div>
|
| 186 |
|
| 187 |
-
<!-- QUESTIONS -->
|
| 188 |
-
<!-- MCQ CARD -->
|
| 189 |
<div class="mcq-card" *ngIf="hasStarted && questions?.length">
|
| 190 |
-
<!-- Header inside the card -->
|
| 191 |
<div class="mcq-card__header">
|
| 192 |
-
<button class="icon-btn back" (click)="goBack()" aria-label="Back">
|
| 193 |
-
<img src="assets/images/reading/back.png" alt="" class="icon-img" />
|
| 194 |
-
</button>
|
| 195 |
|
| 196 |
-
<
|
| 197 |
-
Question {{ currentQuestionIndex + 1 }} of {{ questions.length }}
|
| 198 |
-
</h3>
|
| 199 |
|
|
|
|
| 200 |
<div class="mcq-card__actions">
|
| 201 |
-
<button class="icon
|
| 202 |
</div>
|
| 203 |
</div>
|
| 204 |
-
|
| 205 |
-
<!-- Body -->
|
| 206 |
<div class="mcq-card__body">
|
| 207 |
-
<!-- Question pill -->
|
| 208 |
<div class="quiz-pill quiz-question-pill">
|
| 209 |
<span class="qq-label">Question:</span>
|
| 210 |
<span class="qq-text">{{ questions[currentQuestionIndex].question }}</span>
|
| 211 |
</div>
|
| 212 |
-
|
| 213 |
-
<!-- Options -->
|
| 214 |
<ul class="quiz-options">
|
| 215 |
<li *ngFor="let option of (questions[currentQuestionIndex]?.options | slice:0:4); let i = index">
|
| 216 |
-
<label class="quiz-pill quiz-option-pill"
|
| 217 |
-
[ngClass]="{
|
| 218 |
'is-selected': !questions[currentQuestionIndex].isChecked && getSelectedAnswer() === option,
|
| 219 |
'is-correct': questions[currentQuestionIndex].isChecked && questions[currentQuestionIndex].correct_answer === option,
|
| 220 |
'is-incorrect':questions[currentQuestionIndex].isChecked && getSelectedAnswer() === option && option !== questions[currentQuestionIndex].correct_answer
|
| 221 |
}">
|
| 222 |
-
<input type="radio"
|
| 223 |
-
class="visually-hidden"
|
| 224 |
-
[name]="'q_'+currentQuestionIndex"
|
| 225 |
-
[value]="option"
|
| 226 |
-
[checked]="getSelectedAnswer() === option"
|
| 227 |
-
(change)="setSelectedAnswer(option)"
|
| 228 |
-
[disabled]="questions[currentQuestionIndex].isChecked" />
|
| 229 |
<span class="slot">{{ ['A','B','C','D'][i] }}:</span>
|
| 230 |
<span class="opt-text">{{ option }}</span>
|
| 231 |
</label>
|
| 232 |
</li>
|
| 233 |
</ul>
|
| 234 |
</div>
|
| 235 |
-
|
| 236 |
-
<!-- Footer inside the card -->
|
| 237 |
<div class="mcq-card__footer">
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
[disabled]="!selectedAnswers[questions[currentQuestionIndex].question]">
|
| 244 |
-
Validate
|
| 245 |
-
</button>
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
<!-- Next (for Q1 & Q2 after submit) -->
|
| 249 |
-
<button *ngIf="questions[currentQuestionIndex].isChecked && currentQuestionIndex < questions.length - 1"
|
| 250 |
-
class="next-btn"
|
| 251 |
-
(click)="nextQuestion()">
|
| 252 |
-
Next ▶
|
| 253 |
-
</button>
|
| 254 |
-
|
| 255 |
-
|
| 256 |
</div>
|
| 257 |
</div>
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
</div>
|
| 262 |
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
<!-- Congratulations overlay -->
|
| 267 |
<div class="congrats-overlay" *ngIf="showCongrats" aria-live="polite" aria-modal="true" role="dialog">
|
| 268 |
<div class="congrats-card">
|
| 269 |
<div class="score-badge" aria-label="Your score">
|
| 270 |
Your Score: <span class="score">{{ scoreCorrect }}</span> / <span class="total">{{ scoreTotal }}</span>
|
| 271 |
</div>
|
| 272 |
-
<h2>{{ congratsTitle }}</h2>
|
| 273 |
-
<p>{{ congratsMessage }}</p>
|
| 274 |
-
|
| 275 |
-
<button class="start-over-btn" (click)="startOver()">Start Over</button>
|
| 276 |
</div>
|
| 277 |
</div>
|
| 278 |
-
|
| 279 |
</div>
|
|
|
|
| 1 |
<div class="reading-container">
|
| 2 |
<app-header [title]="'Reading'"></app-header>
|
|
|
|
|
|
|
|
|
|
| 3 |
<img src="assets/images/grammar-bg.png" alt="Background" class="grammar-bg" />
|
| 4 |
|
|
|
|
| 5 |
<div class="main-container" *ngIf="!showCongrats">
|
|
|
|
| 6 |
<section class="intro-section split" *ngIf="!content && !hasStarted">
|
| 7 |
<div class="split-shell">
|
|
|
|
| 8 |
<div class="split-left">
|
| 9 |
+
<img class="intro-illustration" src="assets/images/reading/teacher.png" alt="Reading and quiz illustration" />
|
|
|
|
|
|
|
| 10 |
</div>
|
|
|
|
|
|
|
| 11 |
<div class="split-right">
|
| 12 |
<h1 class="hero-title">Welcome to the Reading Exercise</h1>
|
| 13 |
<p class="hero-copy">
|
| 14 |
+
The Reading component is a simple tool that turns any meaningful topic into a short, age-appropriate passage. It checks the topic first to avoid nonsense and unsafe inputs, then creates clear content at the chosen level (Easy, Medium, or Hard). After
|
| 15 |
+
reading, it generates multiple-choice questions and gives instant feedback. Read-Aloud and A−/A+ controls support different learning needs. This helps students build comprehension and vocabulary, and saves teachers and parents
|
| 16 |
+
time during practice, homework, and revision.
|
|
|
|
|
|
|
|
|
|
| 17 |
</p>
|
|
|
|
|
|
|
| 18 |
<div class="form-row">
|
| 19 |
<label class="row-label">Topic:</label>
|
|
|
|
| 20 |
<div class="input-wrap clearable" [class.locked]="!difficulty">
|
| 21 |
<span class="icon-search" aria-hidden="true"></span>
|
| 22 |
+
<input type="text" class="has-clear" [(ngModel)]="topic" [placeholder]="difficulty ? 'Enter or select a topic' : 'Select a level first'" [disabled]="!difficulty" (focus)="openSuggestions()" (input)="onTyping()" (keydown)="onKeydown($event)" (blur)="hideSuggestionsWithDelay()"
|
| 23 |
+
autocomplete="off" aria-autocomplete="list" [attr.aria-expanded]="showSuggestions && !!difficulty" [attr.aria-disabled]="!difficulty" />
|
| 24 |
+
<button class="clear-btn" *ngIf="difficulty && topic.trim().length" (mousedown)="$event.preventDefault()" (click)="onClearTopic()">×</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
<div class="suggestion-box" *ngIf="difficulty && showSuggestions">
|
| 26 |
<ng-container *ngIf="filteredSuggestions?.length">
|
| 27 |
+
<span *ngFor="let s of filteredSuggestions; let i = index" (mousedown)="selectSuggestion(s)" [class.active]="i === activeIndex">{{ s }}</span>
|
|
|
|
|
|
|
| 28 |
</ng-container>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
</div>
|
| 30 |
</div>
|
|
|
|
|
|
|
| 31 |
<div class="field-hint" *ngIf="!difficulty">
|
| 32 |
+
<span class="lock-icon" aria-hidden="true"></span> Please select a level to enable topic suggestions.
|
|
|
|
| 33 |
</div>
|
| 34 |
</div>
|
|
|
|
|
|
|
| 35 |
<div class="form-row">
|
| 36 |
<label class="row-label">Select Level:</label>
|
|
|
|
| 37 |
<div class="select-wrap">
|
| 38 |
<span class="icon-level" aria-hidden="true"></span>
|
| 39 |
<select [(ngModel)]="difficulty" (ngModelChange)="onDifficultyChange($event)" required>
|
|
|
|
| 41 |
<option [ngValue]="'medium'">Medium</option>
|
| 42 |
<option [ngValue]="'hard'">Hard</option>
|
| 43 |
</select>
|
| 44 |
+
<span class="badge" *ngIf="difficulty" [attr.data-level]="difficulty">{{ difficulty | titlecase }}</span>
|
|
|
|
|
|
|
| 45 |
</div>
|
| 46 |
+
<app-button (click)="generateContent()" [disabled]="!topic.trim() || !difficulty.trim() || isGenerateDisabled">Generate Passage</app-button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
</div>
|
| 48 |
</div>
|
| 49 |
</div>
|
|
|
|
|
|
|
| 50 |
<div class="loader-overlay" *ngIf="isGeneratingContent" role="status" aria-live="polite">
|
| 51 |
<div class="loader">Loading</div>
|
| 52 |
</div>
|
|
|
|
|
|
|
| 53 |
<div class="popup-overlay" *ngIf="showPopup">
|
| 54 |
<div class="popup-content">
|
| 55 |
+
<p>{{ errorMessage || 'We could not create the passage right now. Please try again.' }}</p>
|
| 56 |
+
<app-button (click)="closeErrorPopup()">Close</app-button>
|
|
|
|
|
|
|
|
|
|
| 57 |
</div>
|
| 58 |
</div>
|
| 59 |
</section>
|
| 60 |
|
|
|
|
| 61 |
<div *ngIf="content && !hasStarted" class="reading-card">
|
|
|
|
| 62 |
<div class="reading-head">
|
| 63 |
+
<img src="assets/images/reading/back.png" (click)="goToIntroSection()" alt="" class="icon-img" />
|
|
|
|
|
|
|
|
|
|
| 64 |
<h2 class="reading-title">Let’s Start Reading!</h2>
|
|
|
|
|
|
|
| 65 |
<div class="head-actions">
|
| 66 |
<button class="icon-btn" (click)="decreaseFont()" aria-label="Decrease font">A−</button>
|
| 67 |
<button class="icon-btn" (click)="increaseFont()" aria-label="Increase font">A+</button>
|
| 68 |
+
<button class="icon-btn" [class.active]="isReading || ttsPaused" (click)="toggleReadAloud()" [attr.aria-pressed]="isReading || ttsPaused" aria-label="Read aloud">{{ isReading ? '⏸' : '🔊' }}</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
</div>
|
| 70 |
</div>
|
|
|
|
|
|
|
| 71 |
<div class="reading-meta">
|
| 72 |
<span class="chip chip-topic" *ngIf="normalizedTopic || topic">📚 {{ normalizedTopic || topic }}</span>
|
| 73 |
+
<span class="chip chip-level" [attr.data-level]="difficulty">{{ difficulty | titlecase }}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
</div>
|
|
|
|
|
|
|
| 75 |
<div class="passage-shell">
|
| 76 |
<div class="passage-text" [innerHTML]="transformContent(content)"></div>
|
| 77 |
</div>
|
|
|
|
|
|
|
| 78 |
<div class="reading-actions">
|
| 79 |
+
<app-button (click)="stopReadAloud(); generateQuestions()" [disabled]="isGenerateQuestionDisabled">Generate Questions</app-button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
<div class="loader-overlay" *ngIf="loadingQuestions" aria-live="polite" aria-busy="true">
|
| 81 |
<span class="loader">Loading</span>
|
| 82 |
</div>
|
| 83 |
+
<app-button (click)="stopReadAloud(); resetAll()">Reset</app-button>
|
|
|
|
| 84 |
</div>
|
| 85 |
</div>
|
| 86 |
|
|
|
|
|
|
|
| 87 |
<div class="mcq-card" *ngIf="hasStarted && questions?.length">
|
|
|
|
| 88 |
<div class="mcq-card__header">
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
+
<img src="assets/images/reading/back.png" alt="" class="icon-img" />
|
|
|
|
|
|
|
| 91 |
|
| 92 |
+
<h3 class="mcq-card__title">Question {{ currentQuestionIndex + 1 }} of {{ questions.length }}</h3>
|
| 93 |
<div class="mcq-card__actions">
|
| 94 |
+
<button class="user-guide-close-icon" (click)="startOver()">×</button>
|
| 95 |
</div>
|
| 96 |
</div>
|
|
|
|
|
|
|
| 97 |
<div class="mcq-card__body">
|
|
|
|
| 98 |
<div class="quiz-pill quiz-question-pill">
|
| 99 |
<span class="qq-label">Question:</span>
|
| 100 |
<span class="qq-text">{{ questions[currentQuestionIndex].question }}</span>
|
| 101 |
</div>
|
|
|
|
|
|
|
| 102 |
<ul class="quiz-options">
|
| 103 |
<li *ngFor="let option of (questions[currentQuestionIndex]?.options | slice:0:4); let i = index">
|
| 104 |
+
<label class="quiz-pill quiz-option-pill" [ngClass]="{
|
|
|
|
| 105 |
'is-selected': !questions[currentQuestionIndex].isChecked && getSelectedAnswer() === option,
|
| 106 |
'is-correct': questions[currentQuestionIndex].isChecked && questions[currentQuestionIndex].correct_answer === option,
|
| 107 |
'is-incorrect':questions[currentQuestionIndex].isChecked && getSelectedAnswer() === option && option !== questions[currentQuestionIndex].correct_answer
|
| 108 |
}">
|
| 109 |
+
<input type="radio" class="visually-hidden" [name]="'q_'+currentQuestionIndex" [value]="option" [checked]="getSelectedAnswer() === option" (change)="setSelectedAnswer(option)" [disabled]="questions[currentQuestionIndex].isChecked" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
<span class="slot">{{ ['A','B','C','D'][i] }}:</span>
|
| 111 |
<span class="opt-text">{{ option }}</span>
|
| 112 |
</label>
|
| 113 |
</li>
|
| 114 |
</ul>
|
| 115 |
</div>
|
|
|
|
|
|
|
| 116 |
<div class="mcq-card__footer">
|
| 117 |
+
<app-button *ngIf="currentQuestionIndex > 0" (click)="previousQuestion()">◀ Previous</app-button>
|
| 118 |
+
<div style="display: flex; gap: 10px; justify-content: flex-end; flex: 1;">
|
| 119 |
+
<app-button *ngIf="!questions[currentQuestionIndex].isChecked" (click)="validateAnswer(); scheduleCongratsIfLast()" [disabled]="!selectedAnswers[questions[currentQuestionIndex].question]">Validate</app-button>
|
| 120 |
+
<app-button *ngIf="questions[currentQuestionIndex].isChecked && currentQuestionIndex < questions.length - 1" (click)="nextQuestion()">Next ▶</app-button>
|
| 121 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
</div>
|
| 123 |
</div>
|
|
|
|
|
|
|
|
|
|
| 124 |
</div>
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
<div class="congrats-overlay" *ngIf="showCongrats" aria-live="polite" aria-modal="true" role="dialog">
|
| 127 |
<div class="congrats-card">
|
| 128 |
<div class="score-badge" aria-label="Your score">
|
| 129 |
Your Score: <span class="score">{{ scoreCorrect }}</span> / <span class="total">{{ scoreTotal }}</span>
|
| 130 |
</div>
|
| 131 |
+
<h2>{{ congratsTitle }}</h2>
|
| 132 |
+
<p>{{ congratsMessage }}</p>
|
| 133 |
+
<app-button (click)="startOver()">Start Over</app-button>
|
|
|
|
| 134 |
</div>
|
| 135 |
</div>
|
|
|
|
| 136 |
</div>
|
src/app/reading/reading.component.ts
CHANGED
|
@@ -2,16 +2,19 @@ import { Component, ElementRef, ViewChild } from '@angular/core';
|
|
| 2 |
import { ReadingService } from './reading.service';
|
| 3 |
import { Router } from '@angular/router';
|
| 4 |
import confetti from 'canvas-confetti';
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
@Component({
|
| 8 |
selector: 'app-reading',
|
| 9 |
templateUrl: './reading.component.html',
|
| 10 |
-
styleUrl: './reading.component.css'
|
|
|
|
|
|
|
| 11 |
})
|
| 12 |
-
|
| 13 |
export class ReadingComponent {
|
| 14 |
-
// basic state
|
| 15 |
loadingQuestions = false;
|
| 16 |
isGeneratingContent = false;
|
| 17 |
isGenerateDisabled = false;
|
|
@@ -19,31 +22,25 @@ export class ReadingComponent {
|
|
| 19 |
showPopup = false;
|
| 20 |
showSuggestions = false;
|
| 21 |
|
| 22 |
-
|
| 23 |
-
// quiz state
|
| 24 |
hasStarted = false;
|
| 25 |
currentQuestionIndex = 0;
|
| 26 |
questions: { question: string; options: string[]; correct_answer: string; isChecked?: boolean }[] = [];
|
| 27 |
selectedAnswers: { [key: string]: string } = {};
|
| 28 |
|
| 29 |
-
// content state
|
| 30 |
topic: string = '';
|
| 31 |
difficulty: 'easy' | 'medium' | 'hard' = 'easy';
|
| 32 |
content: string = '';
|
| 33 |
errorMessage: string = '';
|
| 34 |
normalizedTopic: string = '';
|
| 35 |
|
| 36 |
-
// suggestions state
|
| 37 |
filteredSuggestions: string[] = [];
|
| 38 |
activeIndex = -1;
|
| 39 |
|
| 40 |
-
// read-aloud + font
|
| 41 |
fontPx = parseInt(localStorage.getItem('passageFontPx') || '18', 10);
|
| 42 |
isReading = false;
|
| 43 |
ttsPaused = false;
|
| 44 |
private ttsUtterance?: SpeechSynthesisUtterance;
|
| 45 |
|
| 46 |
-
// topic lists
|
| 47 |
topicsByDifficulty: Record<'easy' | 'medium' | 'hard', string[]> = {
|
| 48 |
easy: [
|
| 49 |
'My Family', 'My School', 'My Neighborhood', 'Community Helpers',
|
|
@@ -84,41 +81,26 @@ export class ReadingComponent {
|
|
| 84 |
|
| 85 |
goToIntroSection(): void {
|
| 86 |
this.stopReadAloud?.();
|
| 87 |
-
|
| 88 |
-
// Show Intro by clearing passage, but DO NOT touch topic/difficulty
|
| 89 |
this.content = '';
|
| 90 |
this.hasStarted = false;
|
| 91 |
-
|
| 92 |
-
// On Intro, allow user to click "Generate Passage" again
|
| 93 |
this.isGenerateDisabled = false;
|
| 94 |
-
|
| 95 |
-
// No passage now → disable "Generate Questions"
|
| 96 |
this.refreshGenerateQuestionsState();
|
| 97 |
-
|
| 98 |
-
// Make sure any congrats overlay is closed
|
| 99 |
this.showCongrats = false;
|
| 100 |
}
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
generateContent(): void {
|
| 105 |
this.showPopup = false;
|
| 106 |
this.errorMessage = '';
|
| 107 |
-
|
| 108 |
if (!this.topic.trim() || !this.difficulty.trim()) {
|
| 109 |
this.errorMessage = 'Please enter a topic and select a difficulty level.';
|
| 110 |
this.showPopup = true;
|
| 111 |
return;
|
| 112 |
}
|
| 113 |
-
|
| 114 |
this.isGenerateDisabled = true;
|
| 115 |
this.isGeneratingContent = true;
|
| 116 |
-
|
| 117 |
this.readingService.generateContent(this.topic, this.difficulty).subscribe(
|
| 118 |
(response) => {
|
| 119 |
const text = (response?.content || '').trim();
|
| 120 |
-
|
| 121 |
-
// ✅ Do not check for exact topic substring
|
| 122 |
if (!text) {
|
| 123 |
this.errorMessage = 'The server did not return any content.';
|
| 124 |
this.showPopup = true;
|
|
@@ -128,21 +110,14 @@ export class ReadingComponent {
|
|
| 128 |
this.normalizedTopic = '';
|
| 129 |
return;
|
| 130 |
}
|
| 131 |
-
|
| 132 |
this.content = text;
|
| 133 |
-
|
| 134 |
-
// Support both shapes: { normalized_topic } or { topic } or fallback to input
|
| 135 |
this.normalizedTopic = (response?.normalized_topic || response?.topic || this.topic || '').trim();
|
| 136 |
-
|
| 137 |
this.isGeneratingContent = false;
|
| 138 |
-
|
| 139 |
-
// Show passage screen; enable "Generate Questions"
|
| 140 |
this.hasStarted = false;
|
| 141 |
this.refreshGenerateQuestionsState();
|
| 142 |
},
|
| 143 |
(error) => {
|
| 144 |
console.error(error);
|
| 145 |
-
// ✅ Show backend message if present (your Flask returns {"error": "..."} on 400)
|
| 146 |
const msg = error?.error?.error || 'Invalid topic. Please enter a meaningful topic.';
|
| 147 |
this.errorMessage = msg;
|
| 148 |
this.showPopup = true;
|
|
@@ -152,13 +127,10 @@ export class ReadingComponent {
|
|
| 152 |
);
|
| 153 |
}
|
| 154 |
|
| 155 |
-
|
| 156 |
-
/** Generate MCQs from current passage. */
|
| 157 |
generateQuestions(): void {
|
| 158 |
if (!this.content.trim()) return;
|
| 159 |
this.loadingQuestions = true;
|
| 160 |
this.isGenerateQuestionDisabled = true;
|
| 161 |
-
|
| 162 |
this.readingService.generateQuestions(this.content, this.difficulty).subscribe(
|
| 163 |
(response) => {
|
| 164 |
this.questions = this.parseQuestions(response.questions);
|
|
@@ -171,17 +143,14 @@ export class ReadingComponent {
|
|
| 171 |
(error) => {
|
| 172 |
console.error(error);
|
| 173 |
this.loadingQuestions = false;
|
| 174 |
-
|
| 175 |
-
this.refreshGenerateQuestionsState(); // passage still exists → re-enable button
|
| 176 |
}
|
| 177 |
);
|
| 178 |
}
|
| 179 |
|
| 180 |
-
/** Parse questions from either JSON (preferred) or old plain-text format. */
|
| 181 |
parseQuestions(
|
| 182 |
raw: any
|
| 183 |
): { question: string; options: string[]; correct_answer: string; isChecked?: boolean }[] {
|
| 184 |
-
// 1) If backend returns an array of question objects (JSON)
|
| 185 |
if (Array.isArray(raw)) {
|
| 186 |
return raw
|
| 187 |
.map((q: any) => ({
|
|
@@ -192,55 +161,47 @@ export class ReadingComponent {
|
|
| 192 |
}))
|
| 193 |
.filter(q => q.question && q.options.length === 4);
|
| 194 |
}
|
| 195 |
-
|
| 196 |
-
// 2) Fallback: parse the old plain-text format
|
| 197 |
const out: { question: string; options: string[]; correct_answer: string; isChecked?: boolean }[] = [];
|
| 198 |
const text = String(raw || '');
|
| 199 |
const blocks = text.split('\n\n');
|
| 200 |
-
|
| 201 |
blocks.forEach(block => {
|
| 202 |
const obj: any = {};
|
| 203 |
const lines = block.split('\n');
|
| 204 |
-
|
| 205 |
obj.question = (lines[0] || '').replace(/^(\d+\.\s*)?Question:\s*/i, '').trim();
|
| 206 |
-
|
| 207 |
const optionsText = (lines[1] || '')
|
| 208 |
.replace(/^Options:\s*\[/i, '')
|
| 209 |
.replace(/\]\s*$/, '')
|
| 210 |
.trim();
|
| 211 |
-
|
| 212 |
-
//obj.options = optionsText ? optionsText.split(', ').map(o => o.trim()) : [];
|
| 213 |
-
|
| 214 |
obj.options = optionsText
|
| 215 |
? optionsText.split(', ').map(o => o.trim()).slice(0, 4)
|
| 216 |
: [];
|
| 217 |
obj.correct_answer = (lines[2] || '').replace(/^Correct Answer:\s*/i, '').trim();
|
| 218 |
obj.isChecked = false;
|
| 219 |
-
|
| 220 |
if (obj.question && obj.options?.length) out.push(obj);
|
| 221 |
});
|
| 222 |
-
|
| 223 |
return out;
|
| 224 |
}
|
| 225 |
|
| 226 |
-
/** Record the selected answer for current question. */
|
| 227 |
setSelectedAnswer(value: string): void {
|
| 228 |
const curr = this.questions?.[this.currentQuestionIndex];
|
| 229 |
if (curr) this.selectedAnswers[curr.question] = value;
|
| 230 |
}
|
| 231 |
|
| 232 |
-
/** Get the selected answer for current question. */
|
| 233 |
getSelectedAnswer(): string {
|
| 234 |
const curr = this.questions?.[this.currentQuestionIndex];
|
| 235 |
return (curr && this.selectedAnswers[curr.question]) || '';
|
| 236 |
}
|
| 237 |
|
| 238 |
-
/** Move to next question. */
|
| 239 |
nextQuestion(): void {
|
| 240 |
if (this.currentQuestionIndex < this.questions.length - 1) this.currentQuestionIndex++;
|
| 241 |
}
|
| 242 |
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
private markCurrentAsChecked(): void {
|
| 245 |
const curr = this.questions[this.currentQuestionIndex];
|
| 246 |
if (curr && this.selectedAnswers[curr.question]) {
|
|
@@ -248,73 +209,55 @@ export class ReadingComponent {
|
|
| 248 |
}
|
| 249 |
}
|
| 250 |
|
| 251 |
-
/** Close the error popup. */
|
| 252 |
closeErrorPopup(): void { this.showPopup = false; }
|
| 253 |
|
| 254 |
-
/** Navigate to home route. */
|
| 255 |
goToHome(): void { this.router.navigate(['/home']); }
|
| 256 |
|
| 257 |
-
/** Return to content stage and re-enable input. */
|
| 258 |
goToContentBlock(): void {
|
| 259 |
this.hasStarted = false;
|
| 260 |
this.content = '';
|
| 261 |
this.isGenerateDisabled = false;
|
| 262 |
}
|
| 263 |
|
| 264 |
-
/** Reset everything to initial state. */
|
| 265 |
resetAll(): void {
|
| 266 |
this.initState();
|
| 267 |
this.stopReadAloud?.();
|
| 268 |
-
|
| 269 |
-
// Inputs / choices
|
| 270 |
this.topic = '';
|
| 271 |
this.difficulty = 'easy';
|
| 272 |
this.normalizedTopic = '';
|
| 273 |
-
|
| 274 |
-
// Generated passage and questions
|
| 275 |
this.content = '';
|
| 276 |
this.questions = [];
|
| 277 |
this.currentQuestionIndex = 0;
|
| 278 |
this.selectedAnswers = {};
|
| 279 |
-
|
| 280 |
-
// View flags
|
| 281 |
this.hasStarted = false;
|
| 282 |
this.showCongrats = false;
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
this.isGenerateDisabled = false; // allow new passage generation
|
| 286 |
-
this.isGenerateQuestionDisabled = true; // disabled until a passage exists
|
| 287 |
this.isGeneratingContent = false;
|
| 288 |
this.loadingQuestions = false;
|
| 289 |
this.showPopup = false;
|
| 290 |
this.errorMessage = '';
|
| 291 |
-
|
| 292 |
-
// Score
|
| 293 |
this.scoreCorrect = 0;
|
| 294 |
this.scoreTotal = 0;
|
| 295 |
}
|
| 296 |
|
| 297 |
-
/** Return from MCQ to passage view. */
|
| 298 |
goBack(): void {
|
| 299 |
-
this.hasStarted = false;
|
| 300 |
-
this.refreshGenerateQuestionsState();
|
| 301 |
}
|
| 302 |
|
| 303 |
-
/** Open suggestions for current difficulty. */
|
| 304 |
openSuggestions(): void {
|
| 305 |
this.filterSuggestions();
|
| 306 |
this.showSuggestions = true;
|
| 307 |
this.activeIndex = -1;
|
| 308 |
}
|
| 309 |
|
| 310 |
-
/** Update suggestions on typing. */
|
| 311 |
onTyping(): void {
|
| 312 |
this.filterSuggestions();
|
| 313 |
this.showSuggestions = true;
|
| 314 |
this.activeIndex = -1;
|
| 315 |
}
|
| 316 |
|
| 317 |
-
/** Handle suggestion keyboard navigation. */
|
| 318 |
onKeydown(event: KeyboardEvent): void {
|
| 319 |
if (!this.showSuggestions || !this.filteredSuggestions.length) return;
|
| 320 |
if (event.key === 'ArrowDown') {
|
|
@@ -332,17 +275,14 @@ export class ReadingComponent {
|
|
| 332 |
}
|
| 333 |
}
|
| 334 |
|
| 335 |
-
/** Choose a suggestion and close list. */
|
| 336 |
selectSuggestion(val: string): void {
|
| 337 |
this.topic = val || '';
|
| 338 |
this.showSuggestions = false;
|
| 339 |
this.isGenerateDisabled = false;
|
| 340 |
}
|
| 341 |
|
| 342 |
-
/** Hide suggestions after blur. */
|
| 343 |
hideSuggestionsWithDelay(): void { setTimeout(() => (this.showSuggestions = false), 120); }
|
| 344 |
|
| 345 |
-
/** Internal: filter topics based on query+level. */
|
| 346 |
private filterSuggestions(): void {
|
| 347 |
const q = (this.topic || '').toLowerCase().trim();
|
| 348 |
const pool = this.topicsByDifficulty[this.difficulty || 'medium'] || [];
|
|
@@ -350,7 +290,6 @@ export class ReadingComponent {
|
|
| 350 |
: pool.slice(0, 12);
|
| 351 |
}
|
| 352 |
|
| 353 |
-
/** Clear topic and reopen suggestions. */
|
| 354 |
onClearTopic(): void {
|
| 355 |
this.topic = '';
|
| 356 |
this.errorMessage = '';
|
|
@@ -359,7 +298,6 @@ export class ReadingComponent {
|
|
| 359 |
this.isGenerateDisabled = false;
|
| 360 |
}
|
| 361 |
|
| 362 |
-
/** Change difficulty and refresh suggestions. */
|
| 363 |
onDifficultyChange(val: string): void {
|
| 364 |
this.difficulty = (val || '') as any;
|
| 365 |
if (!this.difficulty) {
|
|
@@ -373,19 +311,15 @@ export class ReadingComponent {
|
|
| 373 |
this.activeIndex = -1;
|
| 374 |
}
|
| 375 |
|
| 376 |
-
/** Apply font size CSS variable. */
|
| 377 |
applyFont(): void {
|
| 378 |
document.documentElement.style.setProperty('--passage-font', `${this.fontPx}px`);
|
| 379 |
localStorage.setItem('passageFontPx', String(this.fontPx));
|
| 380 |
}
|
| 381 |
|
| 382 |
-
/** Decrease font size (min 14px). */
|
| 383 |
decreaseFont(): void { this.fontPx = Math.max(14, this.fontPx - 2); this.applyFont(); }
|
| 384 |
|
| 385 |
-
/** Increase font size (max 30px). */
|
| 386 |
increaseFont(): void { this.fontPx = Math.min(30, this.fontPx + 2); this.applyFont(); }
|
| 387 |
|
| 388 |
-
/** Init defaults for the screen. */
|
| 389 |
private initState(): void {
|
| 390 |
this.hasStarted = false;
|
| 391 |
this.isGeneratingContent = false;
|
|
@@ -407,10 +341,8 @@ export class ReadingComponent {
|
|
| 407 |
this.applyFont();
|
| 408 |
}
|
| 409 |
|
| 410 |
-
/** Lifecycle hook to set initial UI. */
|
| 411 |
ngOnInit(): void { this.initState(); }
|
| 412 |
|
| 413 |
-
/** Build safe HTML for passage body. */
|
| 414 |
transformContent(raw: string): string {
|
| 415 |
try {
|
| 416 |
const passage = this.extractPassage(raw);
|
|
@@ -421,7 +353,6 @@ export class ReadingComponent {
|
|
| 421 |
}
|
| 422 |
}
|
| 423 |
|
| 424 |
-
/** Extract text between "Passage:" and "Summary:". */
|
| 425 |
private extractPassage(raw: string): string {
|
| 426 |
if (!raw) return '';
|
| 427 |
const p = raw.indexOf('Passage:');
|
|
@@ -431,12 +362,10 @@ export class ReadingComponent {
|
|
| 431 |
return slice.trim();
|
| 432 |
}
|
| 433 |
|
| 434 |
-
/** Minimal HTML escape. */
|
| 435 |
private escapeHtml(s: string): string {
|
| 436 |
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
| 437 |
}
|
| 438 |
|
| 439 |
-
/** Toggle speech synthesis for the passage. */
|
| 440 |
toggleReadAloud(): void {
|
| 441 |
if (!this.content || typeof window === 'undefined' || !('speechSynthesis' in window)) {
|
| 442 |
this.errorMessage = 'Read Aloud is not supported in this browser.';
|
|
@@ -444,11 +373,9 @@ export class ReadingComponent {
|
|
| 444 |
return;
|
| 445 |
}
|
| 446 |
const synth = window.speechSynthesis;
|
| 447 |
-
|
| 448 |
if (this.isReading && !synth.paused) { synth.pause(); this.isReading = false; this.ttsPaused = true; return; }
|
| 449 |
if (this.ttsPaused) { synth.resume(); this.isReading = true; this.ttsPaused = false; return; }
|
| 450 |
-
|
| 451 |
-
this.stopReadAloud(); // fresh start
|
| 452 |
const text = this.extractPassage(this.content) || this.content;
|
| 453 |
const u = new SpeechSynthesisUtterance(text);
|
| 454 |
u.rate = 1.0; u.pitch = 1.0; u.lang = 'en-US';
|
|
@@ -459,7 +386,6 @@ export class ReadingComponent {
|
|
| 459 |
this.isReading = true;
|
| 460 |
}
|
| 461 |
|
| 462 |
-
/** Stop speech and clear flags. */
|
| 463 |
stopReadAloud(): void {
|
| 464 |
if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
|
| 465 |
window.speechSynthesis.cancel();
|
|
@@ -469,58 +395,41 @@ export class ReadingComponent {
|
|
| 469 |
this.ttsUtterance = undefined;
|
| 470 |
}
|
| 471 |
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
// add inside the component class
|
| 477 |
showCongrats = false;
|
| 478 |
scoreCorrect = 0;
|
| 479 |
-
|
| 480 |
private confettiInterval: any = null;
|
| 481 |
-
|
| 482 |
-
// scoreCorrect: number = 0; // Correct answers
|
| 483 |
-
scoreTotal: number = 3; // Total number of questions
|
| 484 |
congratsTitle: string = '';
|
| 485 |
congratsMessage: string = '';
|
| 486 |
|
| 487 |
scheduleCongratsIfLast(): void {
|
| 488 |
-
// must be last question AND already marked as checked
|
| 489 |
const isLast = this.currentQuestionIndex === this.questions.length - 1;
|
| 490 |
const curr = this.questions[this.currentQuestionIndex];
|
| 491 |
if (!isLast || !curr?.isChecked) return;
|
| 492 |
-
|
| 493 |
-
// small delay so learners can see the final green/red states first
|
| 494 |
setTimeout(() => {
|
| 495 |
const { correct, total } = this.computeScore();
|
| 496 |
this.scoreCorrect = correct;
|
| 497 |
this.scoreTotal = total;
|
| 498 |
-
|
| 499 |
-
// Determine message based on score
|
| 500 |
if (this.scoreCorrect === this.scoreTotal) {
|
| 501 |
-
this.showCongratsMessage(
|
| 502 |
} else if (this.scoreCorrect > 0) {
|
| 503 |
-
this.showCongratsMessage(
|
| 504 |
} else {
|
| 505 |
-
this.showCongratsMessage(
|
| 506 |
}
|
| 507 |
-
|
| 508 |
this.showCongrats = true;
|
| 509 |
-
this.triggerConfetti();
|
| 510 |
}, 800);
|
| 511 |
}
|
| 512 |
|
| 513 |
-
// Helper function to show the appropriate congratulatory message
|
| 514 |
showCongratsMessage(title: string, message: string) {
|
| 515 |
this.congratsTitle = title;
|
| 516 |
this.congratsMessage = message;
|
| 517 |
}
|
| 518 |
-
|
| 519 |
private computeScore(): { correct: number; total: number } {
|
| 520 |
let correct = 0;
|
| 521 |
const total = this.questions.length;
|
| 522 |
-
|
| 523 |
-
// Uses your existing structures: selectedAnswers[...] and question.correct_answer
|
| 524 |
for (const q of this.questions) {
|
| 525 |
const chosen = this.selectedAnswers[q.question];
|
| 526 |
if (chosen && chosen === q.correct_answer) correct++;
|
|
@@ -528,10 +437,8 @@ export class ReadingComponent {
|
|
| 528 |
return { correct, total };
|
| 529 |
}
|
| 530 |
|
| 531 |
-
|
| 532 |
private triggerConfetti(): void {
|
| 533 |
if (this.confettiInterval) clearInterval(this.confettiInterval);
|
| 534 |
-
|
| 535 |
this.confettiInterval = setInterval(() => {
|
| 536 |
confetti({
|
| 537 |
startVelocity: 30,
|
|
@@ -551,48 +458,28 @@ export class ReadingComponent {
|
|
| 551 |
if (canvas) canvas.remove();
|
| 552 |
}
|
| 553 |
|
| 554 |
-
|
| 555 |
-
|
| 556 |
startOver(): void {
|
| 557 |
this.stopConfetti();
|
| 558 |
this.showCongrats = false;
|
| 559 |
-
this.resetAll();
|
| 560 |
}
|
| 561 |
|
| 562 |
-
|
| 563 |
ngOnDestroy(): void {
|
| 564 |
-
|
| 565 |
this.stopConfetti();
|
| 566 |
}
|
| 567 |
-
|
| 568 |
isValidating = false;
|
| 569 |
-
/** Called by the template. Keeps existing validation, then schedules the final modal if last. */
|
| 570 |
validateAnswer(): void {
|
| 571 |
-
// OPTIONAL: short “validating” hold on the button (remove if not needed)
|
| 572 |
this.isValidating = true;
|
| 573 |
setTimeout(() => (this.isValidating = false), 300);
|
| 574 |
-
|
| 575 |
-
// 👉 IMPORTANT:
|
| 576 |
-
// If you already have a function that validates/marks the current question, call it here.
|
| 577 |
-
// Example:
|
| 578 |
-
// this.checkAnswer(); // or
|
| 579 |
-
// this.markCurrentAsChecked(); // or your own method
|
| 580 |
-
// If you do not have one, the fallback below will simply mark the current question as checked.
|
| 581 |
-
|
| 582 |
const curr = this.questions?.[this.currentQuestionIndex];
|
| 583 |
if (curr && this.selectedAnswers?.[curr.question]) {
|
| 584 |
-
curr.isChecked = true;
|
| 585 |
}
|
| 586 |
-
|
| 587 |
-
// After the question has been marked, if it is the last, open the modal
|
| 588 |
this.scheduleCongratsIfLast();
|
| 589 |
}
|
| 590 |
|
| 591 |
-
|
| 592 |
-
/** Enable/disable "Generate Questions" based on whether a passage exists */
|
| 593 |
private refreshGenerateQuestionsState(): void {
|
| 594 |
this.isGenerateQuestionDisabled = !(this.content && this.content.trim().length > 0);
|
| 595 |
}
|
| 596 |
-
|
| 597 |
-
|
| 598 |
}
|
|
|
|
| 2 |
import { ReadingService } from './reading.service';
|
| 3 |
import { Router } from '@angular/router';
|
| 4 |
import confetti from 'canvas-confetti';
|
| 5 |
+
import { ButtonComponent } from '../shared/button/button.component';
|
| 6 |
+
import { CommonModule } from '@angular/common';
|
| 7 |
+
import { FormsModule } from '@angular/forms';
|
| 8 |
+
import { HeaderComponent } from '../shared/header/header.component';
|
| 9 |
|
| 10 |
@Component({
|
| 11 |
selector: 'app-reading',
|
| 12 |
templateUrl: './reading.component.html',
|
| 13 |
+
styleUrl: './reading.component.css',
|
| 14 |
+
standalone: true,
|
| 15 |
+
imports: [ButtonComponent, CommonModule, FormsModule, HeaderComponent]
|
| 16 |
})
|
|
|
|
| 17 |
export class ReadingComponent {
|
|
|
|
| 18 |
loadingQuestions = false;
|
| 19 |
isGeneratingContent = false;
|
| 20 |
isGenerateDisabled = false;
|
|
|
|
| 22 |
showPopup = false;
|
| 23 |
showSuggestions = false;
|
| 24 |
|
|
|
|
|
|
|
| 25 |
hasStarted = false;
|
| 26 |
currentQuestionIndex = 0;
|
| 27 |
questions: { question: string; options: string[]; correct_answer: string; isChecked?: boolean }[] = [];
|
| 28 |
selectedAnswers: { [key: string]: string } = {};
|
| 29 |
|
|
|
|
| 30 |
topic: string = '';
|
| 31 |
difficulty: 'easy' | 'medium' | 'hard' = 'easy';
|
| 32 |
content: string = '';
|
| 33 |
errorMessage: string = '';
|
| 34 |
normalizedTopic: string = '';
|
| 35 |
|
|
|
|
| 36 |
filteredSuggestions: string[] = [];
|
| 37 |
activeIndex = -1;
|
| 38 |
|
|
|
|
| 39 |
fontPx = parseInt(localStorage.getItem('passageFontPx') || '18', 10);
|
| 40 |
isReading = false;
|
| 41 |
ttsPaused = false;
|
| 42 |
private ttsUtterance?: SpeechSynthesisUtterance;
|
| 43 |
|
|
|
|
| 44 |
topicsByDifficulty: Record<'easy' | 'medium' | 'hard', string[]> = {
|
| 45 |
easy: [
|
| 46 |
'My Family', 'My School', 'My Neighborhood', 'Community Helpers',
|
|
|
|
| 81 |
|
| 82 |
goToIntroSection(): void {
|
| 83 |
this.stopReadAloud?.();
|
|
|
|
|
|
|
| 84 |
this.content = '';
|
| 85 |
this.hasStarted = false;
|
|
|
|
|
|
|
| 86 |
this.isGenerateDisabled = false;
|
|
|
|
|
|
|
| 87 |
this.refreshGenerateQuestionsState();
|
|
|
|
|
|
|
| 88 |
this.showCongrats = false;
|
| 89 |
}
|
| 90 |
|
|
|
|
|
|
|
| 91 |
generateContent(): void {
|
| 92 |
this.showPopup = false;
|
| 93 |
this.errorMessage = '';
|
|
|
|
| 94 |
if (!this.topic.trim() || !this.difficulty.trim()) {
|
| 95 |
this.errorMessage = 'Please enter a topic and select a difficulty level.';
|
| 96 |
this.showPopup = true;
|
| 97 |
return;
|
| 98 |
}
|
|
|
|
| 99 |
this.isGenerateDisabled = true;
|
| 100 |
this.isGeneratingContent = true;
|
|
|
|
| 101 |
this.readingService.generateContent(this.topic, this.difficulty).subscribe(
|
| 102 |
(response) => {
|
| 103 |
const text = (response?.content || '').trim();
|
|
|
|
|
|
|
| 104 |
if (!text) {
|
| 105 |
this.errorMessage = 'The server did not return any content.';
|
| 106 |
this.showPopup = true;
|
|
|
|
| 110 |
this.normalizedTopic = '';
|
| 111 |
return;
|
| 112 |
}
|
|
|
|
| 113 |
this.content = text;
|
|
|
|
|
|
|
| 114 |
this.normalizedTopic = (response?.normalized_topic || response?.topic || this.topic || '').trim();
|
|
|
|
| 115 |
this.isGeneratingContent = false;
|
|
|
|
|
|
|
| 116 |
this.hasStarted = false;
|
| 117 |
this.refreshGenerateQuestionsState();
|
| 118 |
},
|
| 119 |
(error) => {
|
| 120 |
console.error(error);
|
|
|
|
| 121 |
const msg = error?.error?.error || 'Invalid topic. Please enter a meaningful topic.';
|
| 122 |
this.errorMessage = msg;
|
| 123 |
this.showPopup = true;
|
|
|
|
| 127 |
);
|
| 128 |
}
|
| 129 |
|
|
|
|
|
|
|
| 130 |
generateQuestions(): void {
|
| 131 |
if (!this.content.trim()) return;
|
| 132 |
this.loadingQuestions = true;
|
| 133 |
this.isGenerateQuestionDisabled = true;
|
|
|
|
| 134 |
this.readingService.generateQuestions(this.content, this.difficulty).subscribe(
|
| 135 |
(response) => {
|
| 136 |
this.questions = this.parseQuestions(response.questions);
|
|
|
|
| 143 |
(error) => {
|
| 144 |
console.error(error);
|
| 145 |
this.loadingQuestions = false;
|
| 146 |
+
this.refreshGenerateQuestionsState();
|
|
|
|
| 147 |
}
|
| 148 |
);
|
| 149 |
}
|
| 150 |
|
|
|
|
| 151 |
parseQuestions(
|
| 152 |
raw: any
|
| 153 |
): { question: string; options: string[]; correct_answer: string; isChecked?: boolean }[] {
|
|
|
|
| 154 |
if (Array.isArray(raw)) {
|
| 155 |
return raw
|
| 156 |
.map((q: any) => ({
|
|
|
|
| 161 |
}))
|
| 162 |
.filter(q => q.question && q.options.length === 4);
|
| 163 |
}
|
|
|
|
|
|
|
| 164 |
const out: { question: string; options: string[]; correct_answer: string; isChecked?: boolean }[] = [];
|
| 165 |
const text = String(raw || '');
|
| 166 |
const blocks = text.split('\n\n');
|
|
|
|
| 167 |
blocks.forEach(block => {
|
| 168 |
const obj: any = {};
|
| 169 |
const lines = block.split('\n');
|
|
|
|
| 170 |
obj.question = (lines[0] || '').replace(/^(\d+\.\s*)?Question:\s*/i, '').trim();
|
|
|
|
| 171 |
const optionsText = (lines[1] || '')
|
| 172 |
.replace(/^Options:\s*\[/i, '')
|
| 173 |
.replace(/\]\s*$/, '')
|
| 174 |
.trim();
|
|
|
|
|
|
|
|
|
|
| 175 |
obj.options = optionsText
|
| 176 |
? optionsText.split(', ').map(o => o.trim()).slice(0, 4)
|
| 177 |
: [];
|
| 178 |
obj.correct_answer = (lines[2] || '').replace(/^Correct Answer:\s*/i, '').trim();
|
| 179 |
obj.isChecked = false;
|
|
|
|
| 180 |
if (obj.question && obj.options?.length) out.push(obj);
|
| 181 |
});
|
|
|
|
| 182 |
return out;
|
| 183 |
}
|
| 184 |
|
|
|
|
| 185 |
setSelectedAnswer(value: string): void {
|
| 186 |
const curr = this.questions?.[this.currentQuestionIndex];
|
| 187 |
if (curr) this.selectedAnswers[curr.question] = value;
|
| 188 |
}
|
| 189 |
|
|
|
|
| 190 |
getSelectedAnswer(): string {
|
| 191 |
const curr = this.questions?.[this.currentQuestionIndex];
|
| 192 |
return (curr && this.selectedAnswers[curr.question]) || '';
|
| 193 |
}
|
| 194 |
|
|
|
|
| 195 |
nextQuestion(): void {
|
| 196 |
if (this.currentQuestionIndex < this.questions.length - 1) this.currentQuestionIndex++;
|
| 197 |
}
|
| 198 |
|
| 199 |
+
previousQuestion(): void {
|
| 200 |
+
if (this.currentQuestionIndex > 0) {
|
| 201 |
+
this.currentQuestionIndex--;
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
private markCurrentAsChecked(): void {
|
| 206 |
const curr = this.questions[this.currentQuestionIndex];
|
| 207 |
if (curr && this.selectedAnswers[curr.question]) {
|
|
|
|
| 209 |
}
|
| 210 |
}
|
| 211 |
|
|
|
|
| 212 |
closeErrorPopup(): void { this.showPopup = false; }
|
| 213 |
|
|
|
|
| 214 |
goToHome(): void { this.router.navigate(['/home']); }
|
| 215 |
|
|
|
|
| 216 |
goToContentBlock(): void {
|
| 217 |
this.hasStarted = false;
|
| 218 |
this.content = '';
|
| 219 |
this.isGenerateDisabled = false;
|
| 220 |
}
|
| 221 |
|
|
|
|
| 222 |
resetAll(): void {
|
| 223 |
this.initState();
|
| 224 |
this.stopReadAloud?.();
|
|
|
|
|
|
|
| 225 |
this.topic = '';
|
| 226 |
this.difficulty = 'easy';
|
| 227 |
this.normalizedTopic = '';
|
|
|
|
|
|
|
| 228 |
this.content = '';
|
| 229 |
this.questions = [];
|
| 230 |
this.currentQuestionIndex = 0;
|
| 231 |
this.selectedAnswers = {};
|
|
|
|
|
|
|
| 232 |
this.hasStarted = false;
|
| 233 |
this.showCongrats = false;
|
| 234 |
+
this.isGenerateDisabled = false;
|
| 235 |
+
this.isGenerateQuestionDisabled = true;
|
|
|
|
|
|
|
| 236 |
this.isGeneratingContent = false;
|
| 237 |
this.loadingQuestions = false;
|
| 238 |
this.showPopup = false;
|
| 239 |
this.errorMessage = '';
|
|
|
|
|
|
|
| 240 |
this.scoreCorrect = 0;
|
| 241 |
this.scoreTotal = 0;
|
| 242 |
}
|
| 243 |
|
|
|
|
| 244 |
goBack(): void {
|
| 245 |
+
this.hasStarted = false;
|
| 246 |
+
this.refreshGenerateQuestionsState();
|
| 247 |
}
|
| 248 |
|
|
|
|
| 249 |
openSuggestions(): void {
|
| 250 |
this.filterSuggestions();
|
| 251 |
this.showSuggestions = true;
|
| 252 |
this.activeIndex = -1;
|
| 253 |
}
|
| 254 |
|
|
|
|
| 255 |
onTyping(): void {
|
| 256 |
this.filterSuggestions();
|
| 257 |
this.showSuggestions = true;
|
| 258 |
this.activeIndex = -1;
|
| 259 |
}
|
| 260 |
|
|
|
|
| 261 |
onKeydown(event: KeyboardEvent): void {
|
| 262 |
if (!this.showSuggestions || !this.filteredSuggestions.length) return;
|
| 263 |
if (event.key === 'ArrowDown') {
|
|
|
|
| 275 |
}
|
| 276 |
}
|
| 277 |
|
|
|
|
| 278 |
selectSuggestion(val: string): void {
|
| 279 |
this.topic = val || '';
|
| 280 |
this.showSuggestions = false;
|
| 281 |
this.isGenerateDisabled = false;
|
| 282 |
}
|
| 283 |
|
|
|
|
| 284 |
hideSuggestionsWithDelay(): void { setTimeout(() => (this.showSuggestions = false), 120); }
|
| 285 |
|
|
|
|
| 286 |
private filterSuggestions(): void {
|
| 287 |
const q = (this.topic || '').toLowerCase().trim();
|
| 288 |
const pool = this.topicsByDifficulty[this.difficulty || 'medium'] || [];
|
|
|
|
| 290 |
: pool.slice(0, 12);
|
| 291 |
}
|
| 292 |
|
|
|
|
| 293 |
onClearTopic(): void {
|
| 294 |
this.topic = '';
|
| 295 |
this.errorMessage = '';
|
|
|
|
| 298 |
this.isGenerateDisabled = false;
|
| 299 |
}
|
| 300 |
|
|
|
|
| 301 |
onDifficultyChange(val: string): void {
|
| 302 |
this.difficulty = (val || '') as any;
|
| 303 |
if (!this.difficulty) {
|
|
|
|
| 311 |
this.activeIndex = -1;
|
| 312 |
}
|
| 313 |
|
|
|
|
| 314 |
applyFont(): void {
|
| 315 |
document.documentElement.style.setProperty('--passage-font', `${this.fontPx}px`);
|
| 316 |
localStorage.setItem('passageFontPx', String(this.fontPx));
|
| 317 |
}
|
| 318 |
|
|
|
|
| 319 |
decreaseFont(): void { this.fontPx = Math.max(14, this.fontPx - 2); this.applyFont(); }
|
| 320 |
|
|
|
|
| 321 |
increaseFont(): void { this.fontPx = Math.min(30, this.fontPx + 2); this.applyFont(); }
|
| 322 |
|
|
|
|
| 323 |
private initState(): void {
|
| 324 |
this.hasStarted = false;
|
| 325 |
this.isGeneratingContent = false;
|
|
|
|
| 341 |
this.applyFont();
|
| 342 |
}
|
| 343 |
|
|
|
|
| 344 |
ngOnInit(): void { this.initState(); }
|
| 345 |
|
|
|
|
| 346 |
transformContent(raw: string): string {
|
| 347 |
try {
|
| 348 |
const passage = this.extractPassage(raw);
|
|
|
|
| 353 |
}
|
| 354 |
}
|
| 355 |
|
|
|
|
| 356 |
private extractPassage(raw: string): string {
|
| 357 |
if (!raw) return '';
|
| 358 |
const p = raw.indexOf('Passage:');
|
|
|
|
| 362 |
return slice.trim();
|
| 363 |
}
|
| 364 |
|
|
|
|
| 365 |
private escapeHtml(s: string): string {
|
| 366 |
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
| 367 |
}
|
| 368 |
|
|
|
|
| 369 |
toggleReadAloud(): void {
|
| 370 |
if (!this.content || typeof window === 'undefined' || !('speechSynthesis' in window)) {
|
| 371 |
this.errorMessage = 'Read Aloud is not supported in this browser.';
|
|
|
|
| 373 |
return;
|
| 374 |
}
|
| 375 |
const synth = window.speechSynthesis;
|
|
|
|
| 376 |
if (this.isReading && !synth.paused) { synth.pause(); this.isReading = false; this.ttsPaused = true; return; }
|
| 377 |
if (this.ttsPaused) { synth.resume(); this.isReading = true; this.ttsPaused = false; return; }
|
| 378 |
+
this.stopReadAloud();
|
|
|
|
| 379 |
const text = this.extractPassage(this.content) || this.content;
|
| 380 |
const u = new SpeechSynthesisUtterance(text);
|
| 381 |
u.rate = 1.0; u.pitch = 1.0; u.lang = 'en-US';
|
|
|
|
| 386 |
this.isReading = true;
|
| 387 |
}
|
| 388 |
|
|
|
|
| 389 |
stopReadAloud(): void {
|
| 390 |
if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
|
| 391 |
window.speechSynthesis.cancel();
|
|
|
|
| 395 |
this.ttsUtterance = undefined;
|
| 396 |
}
|
| 397 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
showCongrats = false;
|
| 399 |
scoreCorrect = 0;
|
|
|
|
| 400 |
private confettiInterval: any = null;
|
| 401 |
+
scoreTotal: number = 3;
|
|
|
|
|
|
|
| 402 |
congratsTitle: string = '';
|
| 403 |
congratsMessage: string = '';
|
| 404 |
|
| 405 |
scheduleCongratsIfLast(): void {
|
|
|
|
| 406 |
const isLast = this.currentQuestionIndex === this.questions.length - 1;
|
| 407 |
const curr = this.questions[this.currentQuestionIndex];
|
| 408 |
if (!isLast || !curr?.isChecked) return;
|
|
|
|
|
|
|
| 409 |
setTimeout(() => {
|
| 410 |
const { correct, total } = this.computeScore();
|
| 411 |
this.scoreCorrect = correct;
|
| 412 |
this.scoreTotal = total;
|
|
|
|
|
|
|
| 413 |
if (this.scoreCorrect === this.scoreTotal) {
|
| 414 |
+
this.showCongratsMessage('🎉 Congratulations 🎉', 'You have completed all questions with a perfect score!');
|
| 415 |
} else if (this.scoreCorrect > 0) {
|
| 416 |
+
this.showCongratsMessage('Good Job!', 'You did well, but there is room for improvement.');
|
| 417 |
} else {
|
| 418 |
+
this.showCongratsMessage('Keep Trying!', "Don't give up, try again!");
|
| 419 |
}
|
|
|
|
| 420 |
this.showCongrats = true;
|
| 421 |
+
this.triggerConfetti();
|
| 422 |
}, 800);
|
| 423 |
}
|
| 424 |
|
|
|
|
| 425 |
showCongratsMessage(title: string, message: string) {
|
| 426 |
this.congratsTitle = title;
|
| 427 |
this.congratsMessage = message;
|
| 428 |
}
|
| 429 |
+
|
| 430 |
private computeScore(): { correct: number; total: number } {
|
| 431 |
let correct = 0;
|
| 432 |
const total = this.questions.length;
|
|
|
|
|
|
|
| 433 |
for (const q of this.questions) {
|
| 434 |
const chosen = this.selectedAnswers[q.question];
|
| 435 |
if (chosen && chosen === q.correct_answer) correct++;
|
|
|
|
| 437 |
return { correct, total };
|
| 438 |
}
|
| 439 |
|
|
|
|
| 440 |
private triggerConfetti(): void {
|
| 441 |
if (this.confettiInterval) clearInterval(this.confettiInterval);
|
|
|
|
| 442 |
this.confettiInterval = setInterval(() => {
|
| 443 |
confetti({
|
| 444 |
startVelocity: 30,
|
|
|
|
| 458 |
if (canvas) canvas.remove();
|
| 459 |
}
|
| 460 |
|
|
|
|
|
|
|
| 461 |
startOver(): void {
|
| 462 |
this.stopConfetti();
|
| 463 |
this.showCongrats = false;
|
| 464 |
+
this.resetAll();
|
| 465 |
}
|
| 466 |
|
|
|
|
| 467 |
ngOnDestroy(): void {
|
|
|
|
| 468 |
this.stopConfetti();
|
| 469 |
}
|
| 470 |
+
|
| 471 |
isValidating = false;
|
|
|
|
| 472 |
validateAnswer(): void {
|
|
|
|
| 473 |
this.isValidating = true;
|
| 474 |
setTimeout(() => (this.isValidating = false), 300);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
const curr = this.questions?.[this.currentQuestionIndex];
|
| 476 |
if (curr && this.selectedAnswers?.[curr.question]) {
|
| 477 |
+
curr.isChecked = true;
|
| 478 |
}
|
|
|
|
|
|
|
| 479 |
this.scheduleCongratsIfLast();
|
| 480 |
}
|
| 481 |
|
|
|
|
|
|
|
| 482 |
private refreshGenerateQuestionsState(): void {
|
| 483 |
this.isGenerateQuestionDisabled = !(this.content && this.content.trim().length > 0);
|
| 484 |
}
|
|
|
|
|
|
|
| 485 |
}
|
src/app/vocabulary-builder/vocabulary-builder.component.css
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
|
| 2 |
margin-top: 1rem;
|
| 3 |
padding: 1rem;
|
| 4 |
background-color: #f0f4f8;
|
| 5 |
border-radius: 8px;
|
| 6 |
border: 1px solid #cdd5df;
|
| 7 |
background: rgba(255, 255, 255, 0.9);
|
| 8 |
width: 80vw;
|
| 9 |
position: absolute;
|
| 10 |
top: 50%;
|
| 11 |
left: 50%;
|
| 12 |
transform: translate(-50%, -50%);
|
| 13 |
height: 66vh;
|
| 14 |
display: flex;
|
| 15 |
justify-content: space-between;
|
| 16 |
align-items: flex-start;
|
| 17 |
padding: 4vw;
|
| 18 |
gap: 2vw;
|
| 19 |
background-color: #fff;
|
| 20 |
border: 10px solid #009688;
|
| 21 |
border-radius: 1vw;
|
| 22 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 23 |
margin: 2vw auto;
|
| 24 |
max-width: 90%;
|
| 25 |
display: flex;
|
| 26 |
align-items: center;
|
| 27 |
gap: 4vw;
|
| 28 |
width: 50%;
|
| 29 |
font-size: 2.5vw;
|
| 30 |
font-weight: 800;
|
| 31 |
color: #006780;
|
| 32 |
margin-bottom: 20px;
|
| 33 |
text-align: center;
|
| 34 |
width: 100%;
|
| 35 |
height: auto;
|
| 36 |
border-radius: 10px;
|
| 37 |
width: 66%;
|
| 38 |
.description-container p {
|
| 39 |
font-size: 1.4vw;
|
| 40 |
margin-bottom: 30px;
|
| 41 |
text-align: justify;
|
| 42 |
}
|
| 43 |
position: relative;
|
| 44 |
border-radius: 0.5vw;
|
| 45 |
font-size: 1.5vw;
|
| 46 |
transition: background-color 0.3s;
|
| 47 |
width: auto;
|
| 48 |
font-weight: bold;
|
| 49 |
background-color: #006780;
|
| 50 |
border: none;
|
| 51 |
color: white;
|
| 52 |
padding: 15px 32px;
|
| 53 |
text-align: center;
|
| 54 |
text-decoration: none;
|
| 55 |
display: inline-flex;
|
| 56 |
align-items: center;
|
| 57 |
justify-content: center;
|
| 58 |
position: relative;
|
| 59 |
min-width: 5vw;
|
| 60 |
height: 2.9vw;
|
| 61 |
margin-top: 1.5vw;
|
| 62 |
.validate-button:hover {
|
| 63 |
background-color: #bdc3c7;
|
| 64 |
box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19);
|
| 65 |
}
|
| 66 |
.validate-button:disabled {
|
| 67 |
background-color: #ccc;
|
| 68 |
cursor: not-allowed;
|
| 69 |
}
|
| 70 |
position: absolute;
|
| 71 |
border-radius: 0.5vw;
|
| 72 |
font-size: 1.5vw;
|
| 73 |
transition: background-color 0.3s;
|
| 74 |
font-weight: bold;
|
| 75 |
background-color: #006780;
|
| 76 |
border: none;
|
| 77 |
color: white;
|
| 78 |
left: 50%;
|
| 79 |
text-decoration: none;
|
| 80 |
transform: translateX(-50%);
|
| 81 |
padding: 0.5vw;
|
| 82 |
.submit-button:hover {
|
| 83 |
background-color: #bdc3c7;
|
| 84 |
box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19);
|
| 85 |
}
|
| 86 |
.submit-button:disabled {
|
| 87 |
background-color: #ccc;
|
| 88 |
cursor: not-allowed;
|
| 89 |
}
|
| 90 |
background: rgba(255, 255, 255, 0.9);
|
| 91 |
padding: 2vw;
|
| 92 |
border-radius: 1vw;
|
| 93 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 94 |
width: 80vw;
|
| 95 |
position: absolute;
|
| 96 |
top: 54%;
|
| 97 |
left: 50%;
|
| 98 |
transform: translate(-50%, -50%);
|
| 99 |
height: 70vh;
|
| 100 |
border: 10px solid #009688;
|
| 101 |
background-color: #fff;
|
| 102 |
.card2 .content-container {
|
| 103 |
display: flex;
|
| 104 |
justify-content: space-around;
|
| 105 |
align-items: center;
|
| 106 |
height: 100%;
|
| 107 |
}
|
| 108 |
max-width: 100%;
|
| 109 |
height: auto;
|
| 110 |
display: flex;
|
| 111 |
flex-direction: column;
|
| 112 |
align-items: center;
|
| 113 |
justify-content: center;
|
| 114 |
text-align: center;
|
| 115 |
width: 50%;
|
| 116 |
.word-container h2 {
|
| 117 |
font-size: 2.5rem;
|
| 118 |
font-weight: bold;
|
| 119 |
color: #333;
|
| 120 |
margin-top: 2.7vw;
|
| 121 |
}
|
| 122 |
width: 45%;
|
| 123 |
text-align: center;
|
| 124 |
padding: 20px;
|
| 125 |
background: rgba(255, 255, 255, 0.7);
|
| 126 |
border-radius: 10px;
|
| 127 |
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
|
| 128 |
margin-right: 4vw;
|
| 129 |
font-size: 1.2vw;
|
| 130 |
margin-bottom: 20px;
|
| 131 |
font-weight: 700;
|
| 132 |
display: flex;
|
| 133 |
flex-direction: column;
|
| 134 |
align-items: center;
|
| 135 |
background-color: #ccc;
|
| 136 |
color: white;
|
| 137 |
padding: 12px 24px;
|
| 138 |
border-radius: 5px;
|
| 139 |
cursor: pointer;
|
| 140 |
font-size: 1.2rem;
|
| 141 |
margin: 10px;
|
| 142 |
width: 80%;
|
| 143 |
transition: background-color 0.3s ease;
|
| 144 |
background-color: #ffffff;
|
| 145 |
color: #333;
|
| 146 |
border: 2px solid #000000;
|
| 147 |
font-size: 1.1vw;
|
| 148 |
border-radius: 3vw;
|
| 149 |
transition: all 0.3s ease;
|
| 150 |
font-weight: 600;
|
| 151 |
display: flex;
|
| 152 |
align-items: center;
|
| 153 |
justify-content: center;
|
| 154 |
box-shadow: -1px 1vw 0.5vw rgb(0 0 0 / 13%);
|
| 155 |
height: 100%;
|
| 156 |
text-align: center;
|
| 157 |
white-space: normal;
|
| 158 |
word-break: break-word;
|
| 159 |
width: 21vw;
|
| 160 |
/* Make state classes override :hover */
|
| 161 |
.option-btn.correct,
|
| 162 |
.option-btn.incorrect,
|
| 163 |
.option-btn.selected {
|
| 164 |
position: relative; /* keeps layout stable */
|
| 165 |
}
|
| 166 |
/* Correct = green (state wins even on hover) */
|
| 167 |
.option-btn.correct,
|
| 168 |
.option-btn.correct:hover {
|
| 169 |
background: #e8f7ed; /* soft green */
|
| 170 |
border-color: #22c55e; /* green-500 */
|
| 171 |
color: #14532d; /* dark green text */
|
| 172 |
}
|
| 173 |
/* Incorrect = red (state wins even on hover) */
|
| 174 |
.option-btn.incorrect,
|
| 175 |
.option-btn.incorrect:hover {
|
| 176 |
background: #fee2e2; /* soft red */
|
| 177 |
border-color: #ef4444; /* red-500 */
|
| 178 |
color: #7f1d1d; /* dark red text */
|
| 179 |
}
|
| 180 |
/* Selected (pre-validation) = blue (state wins even on hover) */
|
| 181 |
.option-btn.selected,
|
| 182 |
.option-btn.selected:hover {
|
| 183 |
background: #e0f2fe; /* soft blue */
|
| 184 |
border-color: #3b82f6; /* blue-500 */
|
| 185 |
color: #0c4a6e; /* dark blue text */
|
| 186 |
}
|
| 187 |
/* Default hover only when no state class is present */
|
| 188 |
.option-btn:not(.selected):not(.correct):not(.incorrect):hover {
|
| 189 |
background: #f3f4f6; /* light grey hover */
|
| 190 |
}
|
| 191 |
/* Optional: disabled look still visible */
|
| 192 |
.option-btn.disabled,
|
| 193 |
.option-btn:disabled {
|
| 194 |
cursor: not-allowed;
|
| 195 |
filter: grayscale(10%);
|
| 196 |
}
|
| 197 |
.option-btn.picked {
|
| 198 |
outline: 2px dashed rgba(0,0,0,0.25);
|
| 199 |
outline-offset: 2px;
|
| 200 |
}
|
| 201 |
margin-left: 8px;
|
| 202 |
padding: 2px 8px;
|
| 203 |
border-radius: 12px;
|
| 204 |
font-size: 12px;
|
| 205 |
background: rgba(0,0,0,0.06);
|
| 206 |
background: rgba(255, 255, 255, 0.9);
|
| 207 |
padding: 2vw;
|
| 208 |
border-radius: 1vw;
|
| 209 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 210 |
width: 80vw;
|
| 211 |
position: absolute;
|
| 212 |
top: 54%;
|
| 213 |
left: 50%;
|
| 214 |
transform: translate(-50%, -50%);
|
| 215 |
height: 70vh;
|
| 216 |
border: 10px solid #009688;
|
| 217 |
background-color: #fff;
|
| 218 |
list-style-type: disc;
|
| 219 |
padding: 0 5vw;
|
| 220 |
color: #000000;
|
| 221 |
overflow-y: auto;
|
| 222 |
height: 18vw;
|
| 223 |
font-size: 1.5vw;
|
| 224 |
margin-bottom: 30px;
|
| 225 |
text-align: justify;
|
| 226 |
.feedback-list1 li {
|
| 227 |
margin-bottom: 10px;
|
| 228 |
line-height: 1.5;
|
| 229 |
display: list-item;
|
| 230 |
}
|
| 231 |
list-style-type: none !important;
|
| 232 |
text-align: justify;
|
| 233 |
height: 20vw;
|
| 234 |
overflow-y: auto;
|
| 235 |
padding: 0 3vw;
|
| 236 |
margin-bottom: 2vw;
|
| 237 |
font-size: 1.5vw;
|
| 238 |
.feedback-list li {
|
| 239 |
margin-bottom: 1vw;
|
| 240 |
font-weight: 500;
|
| 241 |
}
|
| 242 |
color: green;
|
| 243 |
color: red;
|
| 244 |
0% {
|
| 245 |
transform: translate(-50%, -50%) rotate(0deg);
|
| 246 |
}
|
| 247 |
100% {
|
| 248 |
transform: translate(-50%, -50%) rotate(360deg);
|
| 249 |
}
|
| 250 |
background: rgba(255, 255, 255, 0.9);
|
| 251 |
padding: 2vw;
|
| 252 |
border-radius: 1vw;
|
| 253 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 254 |
width: 80vw;
|
| 255 |
position: absolute;
|
| 256 |
top: 54%;
|
| 257 |
left: 50%;
|
| 258 |
transform: translate(-50%, -50%);
|
| 259 |
height: 70vh;
|
| 260 |
border: 10px solid #009688;
|
| 261 |
background-color: #fff;
|
| 262 |
resize: none
|
| 263 |
font-size: 1.8vw;
|
| 264 |
text-align: center;
|
| 265 |
margin-bottom: 20px;
|
| 266 |
color: #006780;
|
| 267 |
font-weight: 600;
|
| 268 |
display: flex;
|
| 269 |
flex-direction: column;
|
| 270 |
align-items: center;
|
| 271 |
width: 100%;
|
| 272 |
display: flex;
|
| 273 |
justify-content: space-between;
|
| 274 |
align-items: center;
|
| 275 |
width: 80%;
|
| 276 |
max-width: 600px;
|
| 277 |
margin-bottom: 20px;
|
| 278 |
.card4 .sentence-input-group label {
|
| 279 |
font-weight: bold;
|
| 280 |
text-align: left;
|
| 281 |
width: 30%;
|
| 282 |
font-size: 1.7vw;
|
| 283 |
}
|
| 284 |
.card4 .sentence-input-group .input-group {
|
| 285 |
width: 60%;
|
| 286 |
}
|
| 287 |
.card4 .sentence-input-group textarea {
|
| 288 |
width: 138%;
|
| 289 |
height: 100%;
|
| 290 |
font-size: 1.4rem;
|
| 291 |
border: 2px solid #006780;
|
| 292 |
font-family: Arial, sans-serif;
|
| 293 |
border-radius: 8px;
|
| 294 |
padding: 0.4vw;
|
| 295 |
}
|
| 296 |
.card4 .sentence-input-group textarea:focus {
|
| 297 |
outline: none;
|
| 298 |
border: 1px solid #007bff;
|
| 299 |
}
|
| 300 |
background: rgba(255, 255, 255, 0.9);
|
| 301 |
padding: 2vw;
|
| 302 |
border-radius: 1vw;
|
| 303 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 304 |
width: 80vw;
|
| 305 |
position: absolute;
|
| 306 |
top: 54%;
|
| 307 |
left: 50%;
|
| 308 |
transform: translate(-50%, -50%);
|
| 309 |
height: 70vh;
|
| 310 |
border: 10px solid #009688;
|
| 311 |
background-color: #fff;
|
| 312 |
.card5 .feedback-text {
|
| 313 |
font-size: 1.8vw;
|
| 314 |
font-weight: 600;
|
| 315 |
color: #333;
|
| 316 |
margin-top: 1vw;
|
| 317 |
word-wrap: break-word;
|
| 318 |
max-height: 400px;
|
| 319 |
overflow-y: auto;
|
| 320 |
padding-left: 20px;
|
| 321 |
padding-right: 20px;
|
| 322 |
text-align: justify;
|
| 323 |
margin-bottom: 1vw;
|
| 324 |
}
|
| 325 |
position: absolute;
|
| 326 |
top: 1vw;
|
| 327 |
left: 2vw;
|
| 328 |
color: #007bff;
|
| 329 |
font-size: 24px;
|
| 330 |
width: 5vw;
|
| 331 |
cursor: pointer;
|
| 332 |
transition: transform 0.2s ease-in-out;
|
| 333 |
.back-icon:hover {
|
| 334 |
transform: scale(1.1);
|
| 335 |
color: #0056b3;
|
| 336 |
}
|
| 337 |
.back-icon img {
|
| 338 |
width: 30px;
|
| 339 |
height: auto;
|
| 340 |
cursor: pointer;
|
| 341 |
}
|
| 342 |
position: absolute;
|
| 343 |
top: -1vw;
|
| 344 |
right: -1.4vw;
|
| 345 |
width: 36px;
|
| 346 |
height: 36px;
|
| 347 |
display: grid;
|
| 348 |
place-items: center;
|
| 349 |
border-radius: 50%;
|
| 350 |
background: #009688;
|
| 351 |
color: #0f172a;
|
| 352 |
border: 4px solid #009688;
|
| 353 |
font-size: 18px;
|
| 354 |
font-weight: 800;
|
| 355 |
cursor: pointer;
|
| 356 |
box-shadow: 0 4px 10px rgba(0, 0, 0, .15);
|
| 357 |
transition: transform .15s ease, box-shadow .15s ease, background .15s ease;
|
| 358 |
z-index: 10;
|
| 359 |
.icon-btn1:hover {
|
| 360 |
transform: scale(1.06);
|
| 361 |
box-shadow: 0 6px 14px rgba(0,0,0,.22);
|
| 362 |
background: #f7fafc;
|
| 363 |
}
|
| 364 |
.icon-btn1:active {
|
| 365 |
transform: scale(0.98);
|
| 366 |
}
|
| 367 |
.icon-btn1 {
|
| 368 |
width: 50px;
|
| 369 |
height: 50px;
|
| 370 |
font-size: 20px;
|
| 371 |
}
|
| 372 |
position: fixed;
|
| 373 |
top: 0;
|
| 374 |
left: 0;
|
| 375 |
width: 100%;
|
| 376 |
height: 100%;
|
| 377 |
display: flex;
|
| 378 |
justify-content: center;
|
| 379 |
align-items: center;
|
| 380 |
z-index: 1000;
|
| 381 |
font-size: 15px;
|
| 382 |
width: 1.5em;
|
| 383 |
height: 1.5em;
|
| 384 |
border-radius: 50%;
|
| 385 |
position: relative;
|
| 386 |
text-indent: -9999em;
|
| 387 |
animation: mulShdSpin 1.1s infinite ease;
|
| 388 |
transform: translateZ(0);
|
| 389 |
0%, 100% {
|
| 390 |
box-shadow: 0em -3em 0em 0em #808080, 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.5), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.7);
|
| 391 |
}
|
| 392 |
12.5% {
|
| 393 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.7), 2.2em -2.2em 0 0em #808080, 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.5);
|
| 394 |
}
|
| 395 |
25% {
|
| 396 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.5), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.7), 3em 0em 0 0em #808080, 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 397 |
}
|
| 398 |
37.5% {
|
| 399 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.5), 3em 0em 0 0em rgba(128, 128, 128, 0.7), 2.2em 2.2em 0 0em #808080, 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 400 |
}
|
| 401 |
50% {
|
| 402 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.5), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.7), 0em 3em 0 0em #808080, -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 403 |
}
|
| 404 |
62.5% {
|
| 405 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.5), 0em 3em 0 0em rgba(128, 128, 128, 0.7), -2.2em 2.2em 0 0em #808080, -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 406 |
}
|
| 407 |
75% {
|
| 408 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.5), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.7), -3em 0em 0 0em #808080, -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 409 |
}
|
| 410 |
87.5% {
|
| 411 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.5), -3em 0em 0 0em rgba(128, 128, 128, 0.7), -2.2em -2.2em 0 0em #808080;
|
| 412 |
}
|
|
|
|
| 413 |
background: rgba(255, 255, 255, 0.9);
|
| 414 |
width: 80vw;
|
| 415 |
position: absolute;
|
| 416 |
top: 50%;
|
| 417 |
left: 50%;
|
| 418 |
transform: translate(-50%, -50%);
|
| 419 |
height: 66vh;
|
| 420 |
display: flex;
|
| 421 |
justify-content: space-between;
|
| 422 |
align-items: flex-start;
|
| 423 |
padding: 4vw;
|
| 424 |
gap: 2vw;
|
| 425 |
background-color: #fff;
|
| 426 |
border: 10px solid var(--main-accent-color);
|
| 427 |
border-radius: 1vw;
|
| 428 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 429 |
margin: 2vw auto;
|
| 430 |
max-width: 90%;
|
| 431 |
display: flex;
|
| 432 |
align-items: center;
|
| 433 |
gap: 4vw;
|
| 434 |
width: 50%;
|
| 435 |
font-size: 2.5vw;
|
| 436 |
font-weight: 800;
|
| 437 |
color: #006780;
|
| 438 |
margin-bottom: 20px;
|
| 439 |
text-align: center;
|
| 440 |
width: 100%;
|
| 441 |
height: auto;
|
| 442 |
border-radius: 10px;
|
| 443 |
width: 66%;
|
| 444 |
.description-container p {
|
| 445 |
font-size: 1.4vw;
|
| 446 |
margin-bottom: 30px;
|
| 447 |
text-align: justify;
|
| 448 |
}
|
| 449 |
position: relative;
|
| 450 |
border-radius: 0.5vw;
|
| 451 |
font-size: 1.5vw;
|
| 452 |
transition: background-color 0.3s;
|
| 453 |
width: auto;
|
| 454 |
font-weight: bold;
|
| 455 |
background-color: #006780;
|
| 456 |
border: none;
|
| 457 |
color: white;
|
| 458 |
padding: 15px 32px;
|
| 459 |
text-align: center;
|
| 460 |
text-decoration: none;
|
| 461 |
display: inline-flex;
|
| 462 |
align-items: center;
|
| 463 |
justify-content: center;
|
| 464 |
position: relative;
|
| 465 |
min-width: 5vw;
|
| 466 |
height: 2.9vw;
|
| 467 |
margin-top: 1.5vw;
|
| 468 |
.validate-button:hover {
|
| 469 |
background-color: #bdc3c7;
|
| 470 |
box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19);
|
| 471 |
}
|
| 472 |
.validate-button:disabled {
|
| 473 |
background-color: #ccc;
|
| 474 |
cursor: not-allowed;
|
| 475 |
}
|
| 476 |
position: absolute;
|
| 477 |
left: 50%;
|
| 478 |
transform: translateX(-50%);
|
| 479 |
background: rgba(255, 255, 255, 0.9);
|
| 480 |
padding: 2vw;
|
| 481 |
border-radius: 1vw;
|
| 482 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 483 |
width: 80vw;
|
| 484 |
position: absolute;
|
| 485 |
top: 54%;
|
| 486 |
left: 50%;
|
| 487 |
transform: translate(-50%, -50%);
|
| 488 |
height: 70vh;
|
| 489 |
border: 10px solid var(--main-accent-color);
|
| 490 |
background-color: #fff;
|
| 491 |
.card2 .content-container {
|
| 492 |
display: flex;
|
| 493 |
justify-content: space-around;
|
| 494 |
align-items: center;
|
| 495 |
height: 100%;
|
| 496 |
}
|
| 497 |
max-width: 100%;
|
| 498 |
height: auto;
|
| 499 |
display: flex;
|
| 500 |
flex-direction: column;
|
| 501 |
align-items: center;
|
| 502 |
justify-content: center;
|
| 503 |
text-align: center;
|
| 504 |
width: 50%;
|
| 505 |
.word-container h2 {
|
| 506 |
font-size: 2.5rem;
|
| 507 |
font-weight: bold;
|
| 508 |
color: #333;
|
| 509 |
margin-top: 2.7vw;
|
| 510 |
}
|
| 511 |
width: 45%;
|
| 512 |
text-align: center;
|
| 513 |
padding: 20px;
|
| 514 |
background: rgba(255, 255, 255, 0.7);
|
| 515 |
border-radius: 10px;
|
| 516 |
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
|
| 517 |
margin-right: 4vw;
|
| 518 |
font-size: 1.2vw;
|
| 519 |
margin-bottom: 20px;
|
| 520 |
font-weight: 700;
|
| 521 |
display: flex;
|
| 522 |
flex-direction: column;
|
| 523 |
align-items: center;
|
| 524 |
background-color: #ccc;
|
| 525 |
color: white;
|
| 526 |
padding: 12px 24px;
|
| 527 |
border-radius: 5px;
|
| 528 |
cursor: pointer;
|
| 529 |
font-size: 1.2rem;
|
| 530 |
margin: 10px;
|
| 531 |
width: 80%;
|
| 532 |
transition: background-color 0.3s ease;
|
| 533 |
background-color: #ffffff;
|
| 534 |
color: #333;
|
| 535 |
border: 2px solid #000000;
|
| 536 |
font-size: 1.1vw;
|
| 537 |
border-radius: 3vw;
|
| 538 |
transition: all 0.3s ease;
|
| 539 |
font-weight: 600;
|
| 540 |
display: flex;
|
| 541 |
align-items: center;
|
| 542 |
justify-content: center;
|
| 543 |
box-shadow: -1px 1vw 0.5vw rgb(0 0 0 / 13%);
|
| 544 |
height: 100%;
|
| 545 |
text-align: center;
|
| 546 |
white-space: normal;
|
| 547 |
word-break: break-word;
|
| 548 |
width: 21vw;
|
| 549 |
.option-btn.correct,
|
| 550 |
.option-btn.correct:hover {
|
| 551 |
background: #e8f7ed;
|
| 552 |
border-color: #22c55e;
|
| 553 |
color: #14532d;
|
| 554 |
}
|
| 555 |
.option-btn.incorrect,
|
| 556 |
.option-btn.incorrect:hover {
|
| 557 |
background: #fee2e2;
|
| 558 |
border-color: #ef4444;
|
| 559 |
color: #7f1d1d;
|
| 560 |
}
|
| 561 |
.option-btn.selected,
|
| 562 |
.option-btn.selected:hover {
|
| 563 |
background: #e0f2fe;
|
| 564 |
border-color: #3b82f6;
|
| 565 |
color: #0c4a6e;
|
| 566 |
}
|
| 567 |
.option-btn:not(.selected):not(.correct):not(.incorrect):hover {
|
| 568 |
background: #f3f4f6;
|
| 569 |
}
|
| 570 |
.option-btn.disabled,
|
| 571 |
.option-btn:disabled {
|
| 572 |
cursor: not-allowed;
|
| 573 |
filter: grayscale(10%);
|
| 574 |
}
|
| 575 |
.option-btn.picked {
|
| 576 |
outline: 2px dashed rgba(0,0,0,0.25);
|
| 577 |
outline-offset: 2px;
|
| 578 |
}
|
| 579 |
margin-left: 8px;
|
| 580 |
padding: 2px 8px;
|
| 581 |
border-radius: 12px;
|
| 582 |
font-size: 12px;
|
| 583 |
background: rgba(0,0,0,0.06);
|
| 584 |
background: rgba(255, 255, 255, 0.9);
|
| 585 |
padding: 2vw;
|
| 586 |
border-radius: 1vw;
|
| 587 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 588 |
width: 80vw;
|
| 589 |
position: absolute;
|
| 590 |
top: 54%;
|
| 591 |
left: 50%;
|
| 592 |
transform: translate(-50%, -50%);
|
| 593 |
height: 70vh;
|
| 594 |
border: 10px solid var(--main-accent-color);
|
| 595 |
background-color: #fff;
|
| 596 |
list-style-type: disc;
|
| 597 |
padding: 0 5vw;
|
| 598 |
color: #000000;
|
| 599 |
overflow-y: auto;
|
| 600 |
height: 18vw;
|
| 601 |
font-size: 1.5vw;
|
| 602 |
margin-bottom: 30px;
|
| 603 |
text-align: justify;
|
| 604 |
.feedback-list1 li {
|
| 605 |
margin-bottom: 10px;
|
| 606 |
line-height: 1.5;
|
| 607 |
display: list-item;
|
| 608 |
}
|
| 609 |
list-style-type: none !important;
|
| 610 |
text-align: justify;
|
| 611 |
height: 20vw;
|
| 612 |
overflow-y: auto;
|
| 613 |
padding: 0 3vw;
|
| 614 |
margin-bottom: 2vw;
|
| 615 |
font-size: 1.5vw;
|
| 616 |
.feedback-list li {
|
| 617 |
margin-bottom: 1vw;
|
| 618 |
font-weight: 500;
|
| 619 |
}
|
| 620 |
color: green;
|
| 621 |
color: red;
|
| 622 |
background: rgba(255, 255, 255, 0.9);
|
| 623 |
padding: 2vw;
|
| 624 |
border-radius: 1vw;
|
| 625 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 626 |
width: 80vw;
|
| 627 |
position: absolute;
|
| 628 |
top: 54%;
|
| 629 |
left: 50%;
|
| 630 |
transform: translate(-50%, -50%);
|
| 631 |
height: 70vh;
|
| 632 |
border: 10px solid var(--main-accent-color);
|
| 633 |
background-color: #fff;
|
| 634 |
resize: none
|
| 635 |
font-size: 1.8vw;
|
| 636 |
text-align: center;
|
| 637 |
margin-bottom: 20px;
|
| 638 |
color: #006780;
|
| 639 |
font-weight: 600;
|
| 640 |
display: flex;
|
| 641 |
flex-direction: column;
|
| 642 |
align-items: center;
|
| 643 |
width: 100%;
|
| 644 |
display: flex;
|
| 645 |
justify-content: space-between;
|
| 646 |
align-items: center;
|
| 647 |
width: 80%;
|
| 648 |
max-width: 600px;
|
| 649 |
margin-bottom: 20px;
|
| 650 |
.card4 .sentence-input-group label {
|
| 651 |
font-weight: bold;
|
| 652 |
text-align: left;
|
| 653 |
width: 30%;
|
| 654 |
font-size: 1.7vw;
|
| 655 |
}
|
| 656 |
.card4 .sentence-input-group .input-group {
|
| 657 |
width: 60%;
|
| 658 |
}
|
| 659 |
.card4 .sentence-input-group textarea {
|
| 660 |
width: 138%;
|
| 661 |
height: 100%;
|
| 662 |
font-size: 1.4rem;
|
| 663 |
border: 2px solid #006780;
|
| 664 |
font-family: Arial, sans-serif;
|
| 665 |
border-radius: 8px;
|
| 666 |
padding: 0.4vw;
|
| 667 |
}
|
| 668 |
.card4 .sentence-input-group textarea:focus {
|
| 669 |
outline: none;
|
| 670 |
border: 1px solid #007bff;
|
| 671 |
}
|
| 672 |
background: rgba(255, 255, 255, 0.9);
|
| 673 |
padding: 2vw;
|
| 674 |
border-radius: 1vw;
|
| 675 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 676 |
width: 80vw;
|
| 677 |
position: absolute;
|
| 678 |
top: 54%;
|
| 679 |
left: 50%;
|
| 680 |
transform: translate(-50%, -50%);
|
| 681 |
height: 70vh;
|
| 682 |
border: 10px solid var(--main-accent-color);
|
| 683 |
background-color: #fff;
|
| 684 |
.card5 .feedback-text {
|
| 685 |
font-size: 1.8vw;
|
| 686 |
font-weight: 600;
|
| 687 |
color: #333;
|
| 688 |
margin-top: 1vw;
|
| 689 |
word-wrap: break-word;
|
| 690 |
max-height: 400px;
|
| 691 |
overflow-y: auto;
|
| 692 |
padding-left: 20px;
|
| 693 |
padding-right: 20px;
|
| 694 |
text-align: justify;
|
| 695 |
margin-bottom: 1vw;
|
| 696 |
}
|
| 697 |
position: absolute;
|
| 698 |
top: 1vw;
|
| 699 |
left: 2vw;
|
| 700 |
color: #007bff;
|
| 701 |
font-size: 24px;
|
| 702 |
width: 5vw;
|
| 703 |
cursor: pointer;
|
| 704 |
transition: transform 0.2s ease-in-out;
|
| 705 |
.back-icon:hover {
|
| 706 |
transform: scale(1.1);
|
| 707 |
color: #0056b3;
|
| 708 |
}
|
| 709 |
.back-icon img {
|
| 710 |
width: 30px;
|
| 711 |
height: auto;
|
| 712 |
cursor: pointer;
|
| 713 |
}
|
| 714 |
top: -1vw;
|
| 715 |
right: -1.4vw;
|
| 716 |
.icon-btn1 {
|
| 717 |
width: 50px;
|
| 718 |
height: 50px;
|
| 719 |
font-size: 20px;
|
| 720 |
}
|
| 721 |
position: fixed;
|
| 722 |
top: 0;
|
| 723 |
left: 0;
|
| 724 |
width: 100%;
|
| 725 |
height: 100%;
|
| 726 |
display: flex;
|
| 727 |
justify-content: center;
|
| 728 |
align-items: center;
|
| 729 |
z-index: 1000;
|
| 730 |
font-size: 15px;
|
| 731 |
width: 1.5em;
|
| 732 |
height: 1.5em;
|
| 733 |
border-radius: 50%;
|
| 734 |
position: relative;
|
| 735 |
text-indent: -9999em;
|
| 736 |
animation: mulShdSpin 1.1s infinite ease;
|
| 737 |
transform: translateZ(0);
|
| 738 |
0%, 100% {
|
| 739 |
box-shadow: 0em -3em 0em 0em #808080, 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.5), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.7);
|
| 740 |
}
|
| 741 |
12.5% {
|
| 742 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.7), 2.2em -2.2em 0 0em #808080, 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.5);
|
| 743 |
}
|
| 744 |
25% {
|
| 745 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.5), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.7), 3em 0em 0 0em #808080, 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 746 |
}
|
| 747 |
37.5% {
|
| 748 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.5), 3em 0em 0 0em rgba(128, 128, 128, 0.7), 2.2em 2.2em 0 0em #808080, 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 749 |
}
|
| 750 |
50% {
|
| 751 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.5), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.7), 0em 3em 0 0em #808080, -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 752 |
}
|
| 753 |
62.5% {
|
| 754 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.5), 0em 3em 0 0em rgba(128, 128, 128, 0.7), -2.2em 2.2em 0 0em #808080, -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 755 |
}
|
| 756 |
75% {
|
| 757 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.5), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.7), -3em 0em 0 0em #808080, -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 758 |
}
|
| 759 |
87.5% {
|
| 760 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.5), -3em 0em 0 0em rgba(128, 128, 128, 0.7), -2.2em -2.2em 0 0em #808080;
|
| 761 |
}
|
|
|
|
|
|
|
| 1 |
margin-top: 1rem;
|
| 2 |
padding: 1rem;
|
| 3 |
background-color: #f0f4f8;
|
| 4 |
border-radius: 8px;
|
| 5 |
border: 1px solid #cdd5df;
|
| 6 |
background: rgba(255, 255, 255, 0.9);
|
| 7 |
width: 80vw;
|
| 8 |
position: absolute;
|
| 9 |
top: 50%;
|
| 10 |
left: 50%;
|
| 11 |
transform: translate(-50%, -50%);
|
| 12 |
height: 66vh;
|
| 13 |
display: flex;
|
| 14 |
justify-content: space-between;
|
| 15 |
align-items: flex-start;
|
| 16 |
padding: 4vw;
|
| 17 |
gap: 2vw;
|
| 18 |
background-color: #fff;
|
| 19 |
border: 10px solid #009688;
|
| 20 |
border-radius: 1vw;
|
| 21 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 22 |
margin: 2vw auto;
|
| 23 |
max-width: 90%;
|
| 24 |
display: flex;
|
| 25 |
align-items: center;
|
| 26 |
gap: 4vw;
|
| 27 |
width: 50%;
|
| 28 |
font-size: 2.5vw;
|
| 29 |
font-weight: 800;
|
| 30 |
color: #006780;
|
| 31 |
margin-bottom: 20px;
|
| 32 |
text-align: center;
|
| 33 |
width: 100%;
|
| 34 |
height: auto;
|
| 35 |
border-radius: 10px;
|
| 36 |
width: 66%;
|
| 37 |
.description-container p {
|
| 38 |
font-size: 1.4vw;
|
| 39 |
margin-bottom: 30px;
|
| 40 |
text-align: justify;
|
| 41 |
}
|
| 42 |
position: relative;
|
| 43 |
border-radius: 0.5vw;
|
| 44 |
font-size: 1.5vw;
|
| 45 |
transition: background-color 0.3s;
|
| 46 |
width: auto;
|
| 47 |
font-weight: bold;
|
| 48 |
background-color: #006780;
|
| 49 |
border: none;
|
| 50 |
color: white;
|
| 51 |
padding: 15px 32px;
|
| 52 |
text-align: center;
|
| 53 |
text-decoration: none;
|
| 54 |
display: inline-flex;
|
| 55 |
align-items: center;
|
| 56 |
justify-content: center;
|
| 57 |
position: relative;
|
| 58 |
min-width: 5vw;
|
| 59 |
height: 2.9vw;
|
| 60 |
margin-top: 1.5vw;
|
| 61 |
.validate-button:hover {
|
| 62 |
background-color: #bdc3c7;
|
| 63 |
box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19);
|
| 64 |
}
|
| 65 |
.validate-button:disabled {
|
| 66 |
background-color: #ccc;
|
| 67 |
cursor: not-allowed;
|
| 68 |
}
|
| 69 |
position: absolute;
|
| 70 |
border-radius: 0.5vw;
|
| 71 |
font-size: 1.5vw;
|
| 72 |
transition: background-color 0.3s;
|
| 73 |
font-weight: bold;
|
| 74 |
background-color: #006780;
|
| 75 |
border: none;
|
| 76 |
color: white;
|
| 77 |
left: 50%;
|
| 78 |
text-decoration: none;
|
| 79 |
transform: translateX(-50%);
|
| 80 |
padding: 0.5vw;
|
| 81 |
.submit-button:hover {
|
| 82 |
background-color: #bdc3c7;
|
| 83 |
box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19);
|
| 84 |
}
|
| 85 |
.submit-button:disabled {
|
| 86 |
background-color: #ccc;
|
| 87 |
cursor: not-allowed;
|
| 88 |
}
|
| 89 |
background: rgba(255, 255, 255, 0.9);
|
| 90 |
padding: 2vw;
|
| 91 |
border-radius: 1vw;
|
| 92 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 93 |
width: 80vw;
|
| 94 |
position: absolute;
|
| 95 |
top: 54%;
|
| 96 |
left: 50%;
|
| 97 |
transform: translate(-50%, -50%);
|
| 98 |
height: 70vh;
|
| 99 |
border: 10px solid #009688;
|
| 100 |
background-color: #fff;
|
| 101 |
.card2 .content-container {
|
| 102 |
display: flex;
|
| 103 |
justify-content: space-around;
|
| 104 |
align-items: center;
|
| 105 |
height: 100%;
|
| 106 |
}
|
| 107 |
max-width: 100%;
|
| 108 |
height: auto;
|
| 109 |
display: flex;
|
| 110 |
flex-direction: column;
|
| 111 |
align-items: center;
|
| 112 |
justify-content: center;
|
| 113 |
text-align: center;
|
| 114 |
width: 50%;
|
| 115 |
.word-container h2 {
|
| 116 |
font-size: 2.5rem;
|
| 117 |
font-weight: bold;
|
| 118 |
color: #333;
|
| 119 |
margin-top: 2.7vw;
|
| 120 |
}
|
| 121 |
width: 45%;
|
| 122 |
text-align: center;
|
| 123 |
padding: 20px;
|
| 124 |
background: rgba(255, 255, 255, 0.7);
|
| 125 |
border-radius: 10px;
|
| 126 |
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
|
| 127 |
margin-right: 4vw;
|
| 128 |
font-size: 1.2vw;
|
| 129 |
margin-bottom: 20px;
|
| 130 |
font-weight: 700;
|
| 131 |
display: flex;
|
| 132 |
flex-direction: column;
|
| 133 |
align-items: center;
|
| 134 |
background-color: #ccc;
|
| 135 |
color: white;
|
| 136 |
padding: 12px 24px;
|
| 137 |
border-radius: 5px;
|
| 138 |
cursor: pointer;
|
| 139 |
font-size: 1.2rem;
|
| 140 |
margin: 10px;
|
| 141 |
width: 80%;
|
| 142 |
transition: background-color 0.3s ease;
|
| 143 |
background-color: #ffffff;
|
| 144 |
color: #333;
|
| 145 |
border: 2px solid #000000;
|
| 146 |
font-size: 1.1vw;
|
| 147 |
border-radius: 3vw;
|
| 148 |
transition: all 0.3s ease;
|
| 149 |
font-weight: 600;
|
| 150 |
display: flex;
|
| 151 |
align-items: center;
|
| 152 |
justify-content: center;
|
| 153 |
box-shadow: -1px 1vw 0.5vw rgb(0 0 0 / 13%);
|
| 154 |
height: 100%;
|
| 155 |
text-align: center;
|
| 156 |
white-space: normal;
|
| 157 |
word-break: break-word;
|
| 158 |
width: 21vw;
|
| 159 |
/* Make state classes override :hover */
|
| 160 |
.option-btn.correct,
|
| 161 |
.option-btn.incorrect,
|
| 162 |
.option-btn.selected {
|
| 163 |
position: relative; /* keeps layout stable */
|
| 164 |
}
|
| 165 |
/* Correct = green (state wins even on hover) */
|
| 166 |
.option-btn.correct,
|
| 167 |
.option-btn.correct:hover {
|
| 168 |
background: #e8f7ed; /* soft green */
|
| 169 |
border-color: #22c55e; /* green-500 */
|
| 170 |
color: #14532d; /* dark green text */
|
| 171 |
}
|
| 172 |
/* Incorrect = red (state wins even on hover) */
|
| 173 |
.option-btn.incorrect,
|
| 174 |
.option-btn.incorrect:hover {
|
| 175 |
background: #fee2e2; /* soft red */
|
| 176 |
border-color: #ef4444; /* red-500 */
|
| 177 |
color: #7f1d1d; /* dark red text */
|
| 178 |
}
|
| 179 |
/* Selected (pre-validation) = blue (state wins even on hover) */
|
| 180 |
.option-btn.selected,
|
| 181 |
.option-btn.selected:hover {
|
| 182 |
background: #e0f2fe; /* soft blue */
|
| 183 |
border-color: #3b82f6; /* blue-500 */
|
| 184 |
color: #0c4a6e; /* dark blue text */
|
| 185 |
}
|
| 186 |
/* Default hover only when no state class is present */
|
| 187 |
.option-btn:not(.selected):not(.correct):not(.incorrect):hover {
|
| 188 |
background: #f3f4f6; /* light grey hover */
|
| 189 |
}
|
| 190 |
/* Optional: disabled look still visible */
|
| 191 |
.option-btn.disabled,
|
| 192 |
.option-btn:disabled {
|
| 193 |
cursor: not-allowed;
|
| 194 |
filter: grayscale(10%);
|
| 195 |
}
|
| 196 |
.option-btn.picked {
|
| 197 |
outline: 2px dashed rgba(0,0,0,0.25);
|
| 198 |
outline-offset: 2px;
|
| 199 |
}
|
| 200 |
margin-left: 8px;
|
| 201 |
padding: 2px 8px;
|
| 202 |
border-radius: 12px;
|
| 203 |
font-size: 12px;
|
| 204 |
background: rgba(0,0,0,0.06);
|
| 205 |
background: rgba(255, 255, 255, 0.9);
|
| 206 |
padding: 2vw;
|
| 207 |
border-radius: 1vw;
|
| 208 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 209 |
width: 80vw;
|
| 210 |
position: absolute;
|
| 211 |
top: 54%;
|
| 212 |
left: 50%;
|
| 213 |
transform: translate(-50%, -50%);
|
| 214 |
height: 70vh;
|
| 215 |
border: 10px solid #009688;
|
| 216 |
background-color: #fff;
|
| 217 |
list-style-type: disc;
|
| 218 |
padding: 0 5vw;
|
| 219 |
color: #000000;
|
| 220 |
overflow-y: auto;
|
| 221 |
height: 18vw;
|
| 222 |
font-size: 1.5vw;
|
| 223 |
margin-bottom: 30px;
|
| 224 |
text-align: justify;
|
| 225 |
.feedback-list1 li {
|
| 226 |
margin-bottom: 10px;
|
| 227 |
line-height: 1.5;
|
| 228 |
display: list-item;
|
| 229 |
}
|
| 230 |
list-style-type: none !important;
|
| 231 |
text-align: justify;
|
| 232 |
height: 20vw;
|
| 233 |
overflow-y: auto;
|
| 234 |
padding: 0 3vw;
|
| 235 |
margin-bottom: 2vw;
|
| 236 |
font-size: 1.5vw;
|
| 237 |
.feedback-list li {
|
| 238 |
margin-bottom: 1vw;
|
| 239 |
font-weight: 500;
|
| 240 |
}
|
| 241 |
color: green;
|
| 242 |
color: red;
|
| 243 |
0% {
|
| 244 |
transform: translate(-50%, -50%) rotate(0deg);
|
| 245 |
}
|
| 246 |
100% {
|
| 247 |
transform: translate(-50%, -50%) rotate(360deg);
|
| 248 |
}
|
| 249 |
background: rgba(255, 255, 255, 0.9);
|
| 250 |
padding: 2vw;
|
| 251 |
border-radius: 1vw;
|
| 252 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 253 |
width: 80vw;
|
| 254 |
position: absolute;
|
| 255 |
top: 54%;
|
| 256 |
left: 50%;
|
| 257 |
transform: translate(-50%, -50%);
|
| 258 |
height: 70vh;
|
| 259 |
border: 10px solid #009688;
|
| 260 |
background-color: #fff;
|
| 261 |
resize: none
|
| 262 |
font-size: 1.8vw;
|
| 263 |
text-align: center;
|
| 264 |
margin-bottom: 20px;
|
| 265 |
color: #006780;
|
| 266 |
font-weight: 600;
|
| 267 |
display: flex;
|
| 268 |
flex-direction: column;
|
| 269 |
align-items: center;
|
| 270 |
width: 100%;
|
| 271 |
display: flex;
|
| 272 |
justify-content: space-between;
|
| 273 |
align-items: center;
|
| 274 |
width: 80%;
|
| 275 |
max-width: 600px;
|
| 276 |
margin-bottom: 20px;
|
| 277 |
.card4 .sentence-input-group label {
|
| 278 |
font-weight: bold;
|
| 279 |
text-align: left;
|
| 280 |
width: 30%;
|
| 281 |
font-size: 1.7vw;
|
| 282 |
}
|
| 283 |
.card4 .sentence-input-group .input-group {
|
| 284 |
width: 60%;
|
| 285 |
}
|
| 286 |
.card4 .sentence-input-group textarea {
|
| 287 |
width: 138%;
|
| 288 |
height: 100%;
|
| 289 |
font-size: 1.4rem;
|
| 290 |
border: 2px solid #006780;
|
| 291 |
font-family: Arial, sans-serif;
|
| 292 |
border-radius: 8px;
|
| 293 |
padding: 0.4vw;
|
| 294 |
}
|
| 295 |
.card4 .sentence-input-group textarea:focus {
|
| 296 |
outline: none;
|
| 297 |
border: 1px solid #007bff;
|
| 298 |
}
|
| 299 |
background: rgba(255, 255, 255, 0.9);
|
| 300 |
padding: 2vw;
|
| 301 |
border-radius: 1vw;
|
| 302 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 303 |
width: 80vw;
|
| 304 |
position: absolute;
|
| 305 |
top: 54%;
|
| 306 |
left: 50%;
|
| 307 |
transform: translate(-50%, -50%);
|
| 308 |
height: 70vh;
|
| 309 |
border: 10px solid #009688;
|
| 310 |
background-color: #fff;
|
| 311 |
.card5 .feedback-text {
|
| 312 |
font-size: 1.8vw;
|
| 313 |
font-weight: 600;
|
| 314 |
color: #333;
|
| 315 |
margin-top: 1vw;
|
| 316 |
word-wrap: break-word;
|
| 317 |
max-height: 400px;
|
| 318 |
overflow-y: auto;
|
| 319 |
padding-left: 20px;
|
| 320 |
padding-right: 20px;
|
| 321 |
text-align: justify;
|
| 322 |
margin-bottom: 1vw;
|
| 323 |
}
|
| 324 |
position: absolute;
|
| 325 |
top: 1vw;
|
| 326 |
left: 2vw;
|
| 327 |
color: #007bff;
|
| 328 |
font-size: 24px;
|
| 329 |
width: 5vw;
|
| 330 |
cursor: pointer;
|
| 331 |
transition: transform 0.2s ease-in-out;
|
| 332 |
.back-icon:hover {
|
| 333 |
transform: scale(1.1);
|
| 334 |
color: #0056b3;
|
| 335 |
}
|
| 336 |
.back-icon img {
|
| 337 |
width: 30px;
|
| 338 |
height: auto;
|
| 339 |
cursor: pointer;
|
| 340 |
}
|
| 341 |
position: absolute;
|
| 342 |
top: -1vw;
|
| 343 |
right: -1.4vw;
|
| 344 |
width: 36px;
|
| 345 |
height: 36px;
|
| 346 |
display: grid;
|
| 347 |
place-items: center;
|
| 348 |
border-radius: 50%;
|
| 349 |
background: #009688;
|
| 350 |
color: #0f172a;
|
| 351 |
border: 4px solid #009688;
|
| 352 |
font-size: 18px;
|
| 353 |
font-weight: 800;
|
| 354 |
cursor: pointer;
|
| 355 |
box-shadow: 0 4px 10px rgba(0, 0, 0, .15);
|
| 356 |
transition: transform .15s ease, box-shadow .15s ease, background .15s ease;
|
| 357 |
z-index: 10;
|
| 358 |
.icon-btn1:hover {
|
| 359 |
transform: scale(1.06);
|
| 360 |
box-shadow: 0 6px 14px rgba(0,0,0,.22);
|
| 361 |
background: #f7fafc;
|
| 362 |
}
|
| 363 |
.icon-btn1:active {
|
| 364 |
transform: scale(0.98);
|
| 365 |
}
|
| 366 |
.icon-btn1 {
|
| 367 |
width: 50px;
|
| 368 |
height: 50px;
|
| 369 |
font-size: 20px;
|
| 370 |
}
|
| 371 |
position: fixed;
|
| 372 |
top: 0;
|
| 373 |
left: 0;
|
| 374 |
width: 100%;
|
| 375 |
height: 100%;
|
| 376 |
display: flex;
|
| 377 |
justify-content: center;
|
| 378 |
align-items: center;
|
| 379 |
z-index: 1000;
|
| 380 |
font-size: 15px;
|
| 381 |
width: 1.5em;
|
| 382 |
height: 1.5em;
|
| 383 |
border-radius: 50%;
|
| 384 |
position: relative;
|
| 385 |
text-indent: -9999em;
|
| 386 |
animation: mulShdSpin 1.1s infinite ease;
|
| 387 |
transform: translateZ(0);
|
| 388 |
0%, 100% {
|
| 389 |
box-shadow: 0em -3em 0em 0em #808080, 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.5), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.7);
|
| 390 |
}
|
| 391 |
12.5% {
|
| 392 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.7), 2.2em -2.2em 0 0em #808080, 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.5);
|
| 393 |
}
|
| 394 |
25% {
|
| 395 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.5), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.7), 3em 0em 0 0em #808080, 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 396 |
}
|
| 397 |
37.5% {
|
| 398 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.5), 3em 0em 0 0em rgba(128, 128, 128, 0.7), 2.2em 2.2em 0 0em #808080, 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 399 |
}
|
| 400 |
50% {
|
| 401 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.5), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.7), 0em 3em 0 0em #808080, -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 402 |
}
|
| 403 |
62.5% {
|
| 404 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.5), 0em 3em 0 0em rgba(128, 128, 128, 0.7), -2.2em 2.2em 0 0em #808080, -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 405 |
}
|
| 406 |
75% {
|
| 407 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.5), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.7), -3em 0em 0 0em #808080, -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 408 |
}
|
| 409 |
87.5% {
|
| 410 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.5), -3em 0em 0 0em rgba(128, 128, 128, 0.7), -2.2em -2.2em 0 0em #808080;
|
| 411 |
}
|
| 412 |
+
.card1 {
|
| 413 |
background: rgba(255, 255, 255, 0.9);
|
| 414 |
width: 80vw;
|
| 415 |
position: absolute;
|
| 416 |
top: 50%;
|
| 417 |
left: 50%;
|
| 418 |
transform: translate(-50%, -50%);
|
| 419 |
height: 66vh;
|
| 420 |
display: flex;
|
| 421 |
justify-content: space-between;
|
| 422 |
align-items: flex-start;
|
| 423 |
padding: 4vw;
|
| 424 |
gap: 2vw;
|
| 425 |
background-color: #fff;
|
| 426 |
border: 10px solid var(--main-accent-color);
|
| 427 |
border-radius: 1vw;
|
| 428 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 429 |
margin: 2vw auto;
|
| 430 |
max-width: 90%;
|
| 431 |
display: flex;
|
| 432 |
align-items: center;
|
| 433 |
gap: 4vw;
|
| 434 |
width: 50%;
|
| 435 |
font-size: 2.5vw;
|
| 436 |
font-weight: 800;
|
| 437 |
color: #006780;
|
| 438 |
margin-bottom: 20px;
|
| 439 |
text-align: center;
|
| 440 |
width: 100%;
|
| 441 |
height: auto;
|
| 442 |
border-radius: 10px;
|
| 443 |
width: 66%;
|
| 444 |
.description-container p {
|
| 445 |
font-size: 1.4vw;
|
| 446 |
margin-bottom: 30px;
|
| 447 |
text-align: justify;
|
| 448 |
}
|
| 449 |
position: relative;
|
| 450 |
border-radius: 0.5vw;
|
| 451 |
font-size: 1.5vw;
|
| 452 |
transition: background-color 0.3s;
|
| 453 |
width: auto;
|
| 454 |
font-weight: bold;
|
| 455 |
background-color: #006780;
|
| 456 |
border: none;
|
| 457 |
color: white;
|
| 458 |
padding: 15px 32px;
|
| 459 |
text-align: center;
|
| 460 |
text-decoration: none;
|
| 461 |
display: inline-flex;
|
| 462 |
align-items: center;
|
| 463 |
justify-content: center;
|
| 464 |
position: relative;
|
| 465 |
min-width: 5vw;
|
| 466 |
height: 2.9vw;
|
| 467 |
margin-top: 1.5vw;
|
| 468 |
.validate-button:hover {
|
| 469 |
background-color: #bdc3c7;
|
| 470 |
box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24),0 17px 50px 0 rgba(0,0,0,0.19);
|
| 471 |
}
|
| 472 |
.validate-button:disabled {
|
| 473 |
background-color: #ccc;
|
| 474 |
cursor: not-allowed;
|
| 475 |
}
|
| 476 |
position: absolute;
|
| 477 |
left: 50%;
|
| 478 |
transform: translateX(-50%);
|
| 479 |
background: rgba(255, 255, 255, 0.9);
|
| 480 |
padding: 2vw;
|
| 481 |
border-radius: 1vw;
|
| 482 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 483 |
width: 80vw;
|
| 484 |
position: absolute;
|
| 485 |
top: 54%;
|
| 486 |
left: 50%;
|
| 487 |
transform: translate(-50%, -50%);
|
| 488 |
height: 70vh;
|
| 489 |
border: 10px solid var(--main-accent-color);
|
| 490 |
background-color: #fff;
|
| 491 |
.card2 .content-container {
|
| 492 |
display: flex;
|
| 493 |
justify-content: space-around;
|
| 494 |
align-items: center;
|
| 495 |
height: 100%;
|
| 496 |
}
|
| 497 |
max-width: 100%;
|
| 498 |
height: auto;
|
| 499 |
display: flex;
|
| 500 |
flex-direction: column;
|
| 501 |
align-items: center;
|
| 502 |
justify-content: center;
|
| 503 |
text-align: center;
|
| 504 |
width: 50%;
|
| 505 |
.word-container h2 {
|
| 506 |
font-size: 2.5rem;
|
| 507 |
font-weight: bold;
|
| 508 |
color: #333;
|
| 509 |
margin-top: 2.7vw;
|
| 510 |
}
|
| 511 |
width: 45%;
|
| 512 |
text-align: center;
|
| 513 |
padding: 20px;
|
| 514 |
background: rgba(255, 255, 255, 0.7);
|
| 515 |
border-radius: 10px;
|
| 516 |
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
|
| 517 |
margin-right: 4vw;
|
| 518 |
font-size: 1.2vw;
|
| 519 |
margin-bottom: 20px;
|
| 520 |
font-weight: 700;
|
| 521 |
display: flex;
|
| 522 |
flex-direction: column;
|
| 523 |
align-items: center;
|
| 524 |
background-color: #ccc;
|
| 525 |
color: white;
|
| 526 |
padding: 12px 24px;
|
| 527 |
border-radius: 5px;
|
| 528 |
cursor: pointer;
|
| 529 |
font-size: 1.2rem;
|
| 530 |
margin: 10px;
|
| 531 |
width: 80%;
|
| 532 |
transition: background-color 0.3s ease;
|
| 533 |
background-color: #ffffff;
|
| 534 |
color: #333;
|
| 535 |
border: 2px solid #000000;
|
| 536 |
font-size: 1.1vw;
|
| 537 |
border-radius: 3vw;
|
| 538 |
transition: all 0.3s ease;
|
| 539 |
font-weight: 600;
|
| 540 |
display: flex;
|
| 541 |
align-items: center;
|
| 542 |
justify-content: center;
|
| 543 |
box-shadow: -1px 1vw 0.5vw rgb(0 0 0 / 13%);
|
| 544 |
height: 100%;
|
| 545 |
text-align: center;
|
| 546 |
white-space: normal;
|
| 547 |
word-break: break-word;
|
| 548 |
width: 21vw;
|
| 549 |
.option-btn.correct,
|
| 550 |
.option-btn.correct:hover {
|
| 551 |
background: #e8f7ed;
|
| 552 |
border-color: #22c55e;
|
| 553 |
color: #14532d;
|
| 554 |
}
|
| 555 |
.option-btn.incorrect,
|
| 556 |
.option-btn.incorrect:hover {
|
| 557 |
background: #fee2e2;
|
| 558 |
border-color: #ef4444;
|
| 559 |
color: #7f1d1d;
|
| 560 |
}
|
| 561 |
.option-btn.selected,
|
| 562 |
.option-btn.selected:hover {
|
| 563 |
background: #e0f2fe;
|
| 564 |
border-color: #3b82f6;
|
| 565 |
color: #0c4a6e;
|
| 566 |
}
|
| 567 |
.option-btn:not(.selected):not(.correct):not(.incorrect):hover {
|
| 568 |
background: #f3f4f6;
|
| 569 |
}
|
| 570 |
.option-btn.disabled,
|
| 571 |
.option-btn:disabled {
|
| 572 |
cursor: not-allowed;
|
| 573 |
filter: grayscale(10%);
|
| 574 |
}
|
| 575 |
.option-btn.picked {
|
| 576 |
outline: 2px dashed rgba(0,0,0,0.25);
|
| 577 |
outline-offset: 2px;
|
| 578 |
}
|
| 579 |
margin-left: 8px;
|
| 580 |
padding: 2px 8px;
|
| 581 |
border-radius: 12px;
|
| 582 |
font-size: 12px;
|
| 583 |
background: rgba(0,0,0,0.06);
|
| 584 |
background: rgba(255, 255, 255, 0.9);
|
| 585 |
padding: 2vw;
|
| 586 |
border-radius: 1vw;
|
| 587 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 588 |
width: 80vw;
|
| 589 |
position: absolute;
|
| 590 |
top: 54%;
|
| 591 |
left: 50%;
|
| 592 |
transform: translate(-50%, -50%);
|
| 593 |
height: 70vh;
|
| 594 |
border: 10px solid var(--main-accent-color);
|
| 595 |
background-color: #fff;
|
| 596 |
list-style-type: disc;
|
| 597 |
padding: 0 5vw;
|
| 598 |
color: #000000;
|
| 599 |
overflow-y: auto;
|
| 600 |
height: 18vw;
|
| 601 |
font-size: 1.5vw;
|
| 602 |
margin-bottom: 30px;
|
| 603 |
text-align: justify;
|
| 604 |
.feedback-list1 li {
|
| 605 |
margin-bottom: 10px;
|
| 606 |
line-height: 1.5;
|
| 607 |
display: list-item;
|
| 608 |
}
|
| 609 |
list-style-type: none !important;
|
| 610 |
text-align: justify;
|
| 611 |
height: 20vw;
|
| 612 |
overflow-y: auto;
|
| 613 |
padding: 0 3vw;
|
| 614 |
margin-bottom: 2vw;
|
| 615 |
font-size: 1.5vw;
|
| 616 |
.feedback-list li {
|
| 617 |
margin-bottom: 1vw;
|
| 618 |
font-weight: 500;
|
| 619 |
}
|
| 620 |
color: green;
|
| 621 |
color: red;
|
| 622 |
background: rgba(255, 255, 255, 0.9);
|
| 623 |
padding: 2vw;
|
| 624 |
border-radius: 1vw;
|
| 625 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 626 |
width: 80vw;
|
| 627 |
position: absolute;
|
| 628 |
top: 54%;
|
| 629 |
left: 50%;
|
| 630 |
transform: translate(-50%, -50%);
|
| 631 |
height: 70vh;
|
| 632 |
border: 10px solid var(--main-accent-color);
|
| 633 |
background-color: #fff;
|
| 634 |
resize: none
|
| 635 |
font-size: 1.8vw;
|
| 636 |
text-align: center;
|
| 637 |
margin-bottom: 20px;
|
| 638 |
color: #006780;
|
| 639 |
font-weight: 600;
|
| 640 |
display: flex;
|
| 641 |
flex-direction: column;
|
| 642 |
align-items: center;
|
| 643 |
width: 100%;
|
| 644 |
display: flex;
|
| 645 |
justify-content: space-between;
|
| 646 |
align-items: center;
|
| 647 |
width: 80%;
|
| 648 |
max-width: 600px;
|
| 649 |
margin-bottom: 20px;
|
| 650 |
.card4 .sentence-input-group label {
|
| 651 |
font-weight: bold;
|
| 652 |
text-align: left;
|
| 653 |
width: 30%;
|
| 654 |
font-size: 1.7vw;
|
| 655 |
}
|
| 656 |
.card4 .sentence-input-group .input-group {
|
| 657 |
width: 60%;
|
| 658 |
}
|
| 659 |
.card4 .sentence-input-group textarea {
|
| 660 |
width: 138%;
|
| 661 |
height: 100%;
|
| 662 |
font-size: 1.4rem;
|
| 663 |
border: 2px solid #006780;
|
| 664 |
font-family: Arial, sans-serif;
|
| 665 |
border-radius: 8px;
|
| 666 |
padding: 0.4vw;
|
| 667 |
}
|
| 668 |
.card4 .sentence-input-group textarea:focus {
|
| 669 |
outline: none;
|
| 670 |
border: 1px solid #007bff;
|
| 671 |
}
|
| 672 |
background: rgba(255, 255, 255, 0.9);
|
| 673 |
padding: 2vw;
|
| 674 |
border-radius: 1vw;
|
| 675 |
box-shadow: 0 0.4vw 0.8vw rgba(0, 0, 0, 0.6);
|
| 676 |
width: 80vw;
|
| 677 |
position: absolute;
|
| 678 |
top: 54%;
|
| 679 |
left: 50%;
|
| 680 |
transform: translate(-50%, -50%);
|
| 681 |
height: 70vh;
|
| 682 |
border: 10px solid var(--main-accent-color);
|
| 683 |
background-color: #fff;
|
| 684 |
.card5 .feedback-text {
|
| 685 |
font-size: 1.8vw;
|
| 686 |
font-weight: 600;
|
| 687 |
color: #333;
|
| 688 |
margin-top: 1vw;
|
| 689 |
word-wrap: break-word;
|
| 690 |
max-height: 400px;
|
| 691 |
overflow-y: auto;
|
| 692 |
padding-left: 20px;
|
| 693 |
padding-right: 20px;
|
| 694 |
text-align: justify;
|
| 695 |
margin-bottom: 1vw;
|
| 696 |
}
|
| 697 |
position: absolute;
|
| 698 |
top: 1vw;
|
| 699 |
left: 2vw;
|
| 700 |
color: #007bff;
|
| 701 |
font-size: 24px;
|
| 702 |
width: 5vw;
|
| 703 |
cursor: pointer;
|
| 704 |
transition: transform 0.2s ease-in-out;
|
| 705 |
.back-icon:hover {
|
| 706 |
transform: scale(1.1);
|
| 707 |
color: #0056b3;
|
| 708 |
}
|
| 709 |
.back-icon img {
|
| 710 |
width: 30px;
|
| 711 |
height: auto;
|
| 712 |
cursor: pointer;
|
| 713 |
}
|
| 714 |
top: -1vw;
|
| 715 |
right: -1.4vw;
|
| 716 |
.icon-btn1 {
|
| 717 |
width: 50px;
|
| 718 |
height: 50px;
|
| 719 |
font-size: 20px;
|
| 720 |
}
|
| 721 |
position: fixed;
|
| 722 |
top: 0;
|
| 723 |
left: 0;
|
| 724 |
width: 100%;
|
| 725 |
height: 100%;
|
| 726 |
display: flex;
|
| 727 |
justify-content: center;
|
| 728 |
align-items: center;
|
| 729 |
z-index: 1000;
|
| 730 |
font-size: 15px;
|
| 731 |
width: 1.5em;
|
| 732 |
height: 1.5em;
|
| 733 |
border-radius: 50%;
|
| 734 |
position: relative;
|
| 735 |
text-indent: -9999em;
|
| 736 |
animation: mulShdSpin 1.1s infinite ease;
|
| 737 |
transform: translateZ(0);
|
| 738 |
0%, 100% {
|
| 739 |
box-shadow: 0em -3em 0em 0em #808080, 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.5), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.7);
|
| 740 |
}
|
| 741 |
12.5% {
|
| 742 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.7), 2.2em -2.2em 0 0em #808080, 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.5);
|
| 743 |
}
|
| 744 |
25% {
|
| 745 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.5), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.7), 3em 0em 0 0em #808080, 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 746 |
}
|
| 747 |
37.5% {
|
| 748 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.5), 3em 0em 0 0em rgba(128, 128, 128, 0.7), 2.2em 2.2em 0 0em #808080, 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 749 |
}
|
| 750 |
50% {
|
| 751 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.5), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.7), 0em 3em 0 0em #808080, -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 752 |
}
|
| 753 |
62.5% {
|
| 754 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.5), 0em 3em 0 0em rgba(128, 128, 128, 0.7), -2.2em 2.2em 0 0em #808080, -3em 0em 0 0em rgba(128, 128, 128, 0.2), -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 755 |
}
|
| 756 |
75% {
|
| 757 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.5), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.7), -3em 0em 0 0em #808080, -2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2);
|
| 758 |
}
|
| 759 |
87.5% {
|
| 760 |
box-shadow: 0em -3em 0em 0em rgba(128, 128, 128, 0.2), 2.2em -2.2em 0 0em rgba(128, 128, 128, 0.2), 3em 0em 0 0em rgba(128, 128, 128, 0.2), 2.2em 2.2em 0 0em rgba(128, 128, 128, 0.2), 0em 3em 0 0em rgba(128, 128, 128, 0.2), -2.2em 2.2em 0 0em rgba(128, 128, 128, 0.5), -3em 0em 0 0em rgba(128, 128, 128, 0.7), -2.2em -2.2em 0 0em #808080;
|
| 761 |
}
|
src/app/vocabulary-builder/vocabulary-builder.component.html
CHANGED
|
@@ -3,7 +3,6 @@
|
|
| 3 |
|
| 4 |
<app-header [title]="'Vocabulary Builder'"></app-header>
|
| 5 |
|
| 6 |
-
|
| 7 |
<!-- Background Image -->
|
| 8 |
<img src="assets/images/grammar-bg.png" alt="Chat Background" class="grammar-bg" />
|
| 9 |
|
|
@@ -19,12 +18,11 @@
|
|
| 19 |
Vocabulary Builder is an interactive learning tool designed to help users strengthen their vocabulary in an engaging and structured manner.
|
| 20 |
It presents a series of word-based exercises that allow users to learn the meaning and usage of words through intelligent suggestions, related word selections, and sentence formation tasks.
|
| 21 |
</p>
|
| 22 |
-
<button
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
[class.loading]="isLoaderVisible">
|
| 26 |
<span>Let's Build</span>
|
| 27 |
-
</button>
|
| 28 |
<div *ngIf="isLoaderVisible" class="loader-overlay">
|
| 29 |
<div class="loader"></div>
|
| 30 |
</div>
|
|
@@ -35,8 +33,9 @@
|
|
| 35 |
<!-- Step 2: Word Options -->
|
| 36 |
<div *ngIf="step === 2" class="card2">
|
| 37 |
<img src="assets/images/vocabulary/back.png" alt="Go Back" class="back-icon" (click)="goBack()" />
|
| 38 |
-
|
| 39 |
-
<button class="icon
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
<div class="content-container">
|
|
@@ -56,31 +55,27 @@
|
|
| 56 |
'correct': optionStates[option] === 'correct',
|
| 57 |
'incorrect': optionStates[option] === 'incorrect',
|
| 58 |
'picked': isChecked && pickedAtCheck.has(option)
|
| 59 |
-
|
| 60 |
}"
|
| 61 |
[disabled]="(selectedOptions.length === 3 && !selectedOptions.includes(option) && buttonState === 'validate') || buttonState !== 'validate'"
|
| 62 |
(click)="handleOptionClick(option)">
|
| 63 |
<span class="label">{{ option }}</span>
|
| 64 |
<span *ngIf="isChecked && pickedAtCheck?.has(option)" class="picked-chip"></span>
|
| 65 |
</button>
|
| 66 |
-
|
| 67 |
-
|
| 68 |
</div>
|
| 69 |
|
| 70 |
-
<button *ngIf="buttonState === 'validate'"
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
(click)="checkSelections()">
|
| 74 |
<span>Validate</span>
|
| 75 |
-
</button>
|
| 76 |
|
| 77 |
-
<button *ngIf="buttonState === 'next'"
|
| 78 |
<span>Next</span>
|
| 79 |
-
</button>
|
| 80 |
|
| 81 |
-
<button *ngIf="buttonState === 'formSentence'"
|
| 82 |
<span>Form a Sentence</span>
|
| 83 |
-
</button>
|
| 84 |
|
| 85 |
<div *ngIf="isLoaderVisible" class="loader-overlay">
|
| 86 |
<div class="loader"></div>
|
|
@@ -93,7 +88,8 @@
|
|
| 93 |
<div *ngIf="step === 3" class="card3">
|
| 94 |
<img src="assets/images/vocabulary/back.png" alt="Go Back" class="back-icon" (click)="goBack()" />
|
| 95 |
<!-- Top-right close button (NOT on intro/page 1) -->
|
| 96 |
-
|
|
|
|
| 97 |
|
| 98 |
<h2>Feedback</h2>
|
| 99 |
<ul class="feedback-list">
|
|
@@ -101,14 +97,14 @@
|
|
| 101 |
<li *ngIf="point.includes('❌')">{{ point }}</li>
|
| 102 |
</ng-container>
|
| 103 |
</ul>
|
| 104 |
-
<button class="submit-button" (click)="startSentenceFormation()">Form a Sentence</button>
|
| 105 |
</div>
|
| 106 |
|
| 107 |
<!-- Step 4: Sentence Formation -->
|
| 108 |
<div *ngIf="step === 4" class="card4">
|
| 109 |
<img src="assets/images/vocabulary/back.png" alt="Go Back" class="back-icon" (click)="goBack()" />
|
| 110 |
<!-- Top-right close button (NOT on intro/page 1) -->
|
| 111 |
-
<button class="icon
|
| 112 |
|
| 113 |
<h2>Form a Sentence</h2>
|
| 114 |
<p class="correct-answer">Using the words: <strong class="text-green-500">{{ correctOptions.join(', ') }}</strong></p>
|
|
@@ -124,11 +120,11 @@
|
|
| 124 |
</div>
|
| 125 |
</div>
|
| 126 |
</div>
|
| 127 |
-
<button class="submit-button"
|
| 128 |
-
|
| 129 |
-
|
| 130 |
<span>Check Sentence</span>
|
| 131 |
-
</button>
|
| 132 |
<div *ngIf="isLoaderVisible" class="loader-overlay">
|
| 133 |
<div class="loader"></div>
|
| 134 |
</div>
|
|
@@ -138,7 +134,7 @@
|
|
| 138 |
<div *ngIf="step === 5" class="card5">
|
| 139 |
<img src="assets/images/vocabulary/back.png" alt="Go Back" class="back-icon" (click)="goBack()" />
|
| 140 |
<!-- Top-right close button (NOT on intro/page 1) -->
|
| 141 |
-
<button class="icon
|
| 142 |
|
| 143 |
<h2 class="feedback-header">Sentence Feedback</h2>
|
| 144 |
<ul class="feedback-list1">
|
|
@@ -149,7 +145,7 @@
|
|
| 149 |
<span *ngIf="!point.includes('correct answer')">{{ point }}</span>
|
| 150 |
</li>
|
| 151 |
</ul>
|
| 152 |
-
<button class="submit-button" (click)="resetQuiz()"> Reset </button>
|
| 153 |
</div>
|
| 154 |
|
| 155 |
</div>
|
|
|
|
| 3 |
|
| 4 |
<app-header [title]="'Vocabulary Builder'"></app-header>
|
| 5 |
|
|
|
|
| 6 |
<!-- Background Image -->
|
| 7 |
<img src="assets/images/grammar-bg.png" alt="Chat Background" class="grammar-bg" />
|
| 8 |
|
|
|
|
| 18 |
Vocabulary Builder is an interactive learning tool designed to help users strengthen their vocabulary in an engaging and structured manner.
|
| 19 |
It presents a series of word-based exercises that allow users to learn the meaning and usage of words through intelligent suggestions, related word selections, and sentence formation tasks.
|
| 20 |
</p>
|
| 21 |
+
<app-button [disabled]="isButtonDisabled || isLoaderVisible"
|
| 22 |
+
[ngClass]="{ loading: isLoaderVisible }"
|
| 23 |
+
(click)="getWords()">
|
|
|
|
| 24 |
<span>Let's Build</span>
|
| 25 |
+
</app-button>
|
| 26 |
<div *ngIf="isLoaderVisible" class="loader-overlay">
|
| 27 |
<div class="loader"></div>
|
| 28 |
</div>
|
|
|
|
| 33 |
<!-- Step 2: Word Options -->
|
| 34 |
<div *ngIf="step === 2" class="card2">
|
| 35 |
<img src="assets/images/vocabulary/back.png" alt="Go Back" class="back-icon" (click)="goBack()" />
|
| 36 |
+
|
| 37 |
+
<button class="user-guide-close-icon" (click)="closeToMain()">×</button>
|
| 38 |
+
|
| 39 |
|
| 40 |
|
| 41 |
<div class="content-container">
|
|
|
|
| 55 |
'correct': optionStates[option] === 'correct',
|
| 56 |
'incorrect': optionStates[option] === 'incorrect',
|
| 57 |
'picked': isChecked && pickedAtCheck.has(option)
|
|
|
|
| 58 |
}"
|
| 59 |
[disabled]="(selectedOptions.length === 3 && !selectedOptions.includes(option) && buttonState === 'validate') || buttonState !== 'validate'"
|
| 60 |
(click)="handleOptionClick(option)">
|
| 61 |
<span class="label">{{ option }}</span>
|
| 62 |
<span *ngIf="isChecked && pickedAtCheck?.has(option)" class="picked-chip"></span>
|
| 63 |
</button>
|
|
|
|
|
|
|
| 64 |
</div>
|
| 65 |
|
| 66 |
+
<app-button *ngIf="buttonState === 'validate'"
|
| 67 |
+
[disabled]="!isCheckButtonEnabled || isLoaderVisible"
|
| 68 |
+
(click)="checkSelections()">
|
|
|
|
| 69 |
<span>Validate</span>
|
| 70 |
+
</app-button>
|
| 71 |
|
| 72 |
+
<app-button *ngIf="buttonState === 'next'" (click)="goNext()">
|
| 73 |
<span>Next</span>
|
| 74 |
+
</app-button>
|
| 75 |
|
| 76 |
+
<app-button *ngIf="buttonState === 'formSentence'" (click)="startSentenceFormation()">
|
| 77 |
<span>Form a Sentence</span>
|
| 78 |
+
</app-button>
|
| 79 |
|
| 80 |
<div *ngIf="isLoaderVisible" class="loader-overlay">
|
| 81 |
<div class="loader"></div>
|
|
|
|
| 88 |
<div *ngIf="step === 3" class="card3">
|
| 89 |
<img src="assets/images/vocabulary/back.png" alt="Go Back" class="back-icon" (click)="goBack()" />
|
| 90 |
<!-- Top-right close button (NOT on intro/page 1) -->
|
| 91 |
+
<!--<button class="icon-btn1" (click)="closeToMain()" aria-label="Close">✖</button>-->
|
| 92 |
+
<button class="user-guide-close-icon" (click)="closeToMain()">×</button>
|
| 93 |
|
| 94 |
<h2>Feedback</h2>
|
| 95 |
<ul class="feedback-list">
|
|
|
|
| 97 |
<li *ngIf="point.includes('❌')">{{ point }}</li>
|
| 98 |
</ng-container>
|
| 99 |
</ul>
|
| 100 |
+
<app-button class="submit-button" (click)="startSentenceFormation()">Form a Sentence</app-button>
|
| 101 |
</div>
|
| 102 |
|
| 103 |
<!-- Step 4: Sentence Formation -->
|
| 104 |
<div *ngIf="step === 4" class="card4">
|
| 105 |
<img src="assets/images/vocabulary/back.png" alt="Go Back" class="back-icon" (click)="goBack()" />
|
| 106 |
<!-- Top-right close button (NOT on intro/page 1) -->
|
| 107 |
+
<button class="user-guide-close-icon" (click)="closeToMain()">×</button>
|
| 108 |
|
| 109 |
<h2>Form a Sentence</h2>
|
| 110 |
<p class="correct-answer">Using the words: <strong class="text-green-500">{{ correctOptions.join(', ') }}</strong></p>
|
|
|
|
| 120 |
</div>
|
| 121 |
</div>
|
| 122 |
</div>
|
| 123 |
+
<app-button class="submit-button"
|
| 124 |
+
[disabled]="!allFieldsFilled() || isLoaderVisible"
|
| 125 |
+
(click)="checkSentence()">
|
| 126 |
<span>Check Sentence</span>
|
| 127 |
+
</app-button>
|
| 128 |
<div *ngIf="isLoaderVisible" class="loader-overlay">
|
| 129 |
<div class="loader"></div>
|
| 130 |
</div>
|
|
|
|
| 134 |
<div *ngIf="step === 5" class="card5">
|
| 135 |
<img src="assets/images/vocabulary/back.png" alt="Go Back" class="back-icon" (click)="goBack()" />
|
| 136 |
<!-- Top-right close button (NOT on intro/page 1) -->
|
| 137 |
+
<button class="user-guide-close-icon" (click)="closeToMain()">×</button>
|
| 138 |
|
| 139 |
<h2 class="feedback-header">Sentence Feedback</h2>
|
| 140 |
<ul class="feedback-list1">
|
|
|
|
| 145 |
<span *ngIf="!point.includes('correct answer')">{{ point }}</span>
|
| 146 |
</li>
|
| 147 |
</ul>
|
| 148 |
+
<app-button class="submit-button" (click)="resetQuiz()"> Reset </app-button>
|
| 149 |
</div>
|
| 150 |
|
| 151 |
</div>
|
src/app/vocabulary-builder/vocabulary-builder.component.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
| 1 |
import { Component, OnInit, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
|
| 2 |
import { VocabularyBuilderService } from './vocabulary-builder.service';
|
| 3 |
import { Router } from '@angular/router';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
@Component({
|
| 6 |
selector: 'app-vocabulary-builder',
|
| 7 |
templateUrl: './vocabulary-builder.component.html',
|
| 8 |
-
styleUrls: ['./vocabulary-builder.component.css']
|
|
|
|
|
|
|
| 9 |
})
|
| 10 |
export class VocabularyBuilderComponent implements OnInit, AfterViewInit {
|
| 11 |
@ViewChild('step1', { static: false }) step1?: ElementRef;
|
|
@@ -87,19 +93,15 @@ export class VocabularyBuilderComponent implements OnInit, AfterViewInit {
|
|
| 87 |
|
| 88 |
const i = this.selectedOptions.indexOf(option);
|
| 89 |
if (i >= 0) {
|
| 90 |
-
// toggle OFF
|
| 91 |
this.selectedOptions.splice(i, 1);
|
| 92 |
} else {
|
| 93 |
-
// do NOT auto-deselect the first; just stop at 3
|
| 94 |
if (this.selectedOptions.length >= 3) return;
|
| 95 |
this.selectedOptions.push(option);
|
| 96 |
}
|
| 97 |
|
| 98 |
-
// Enable Validate only when exactly 3 selected
|
| 99 |
this.isCheckButtonEnabled = this.selectedOptions.length === 3;
|
| 100 |
}
|
| 101 |
|
| 102 |
-
|
| 103 |
checkSelections(): void {
|
| 104 |
if (this.buttonState === 'next') {
|
| 105 |
this.step = 3;
|
|
@@ -117,7 +119,6 @@ export class VocabularyBuilderComponent implements OnInit, AfterViewInit {
|
|
| 117 |
this.processFeedback(response.feedback);
|
| 118 |
this.isChecked = true;
|
| 119 |
this.correctAnswers = response.correctAnswers || [];
|
| 120 |
-
// ⬇️ INSERT THIS LINE HERE
|
| 121 |
this.pickedAtCheck = new Set(this.selectedOptions);
|
| 122 |
|
| 123 |
this.optionStates = {};
|
|
@@ -305,14 +306,9 @@ export class VocabularyBuilderComponent implements OnInit, AfterViewInit {
|
|
| 305 |
this.buttonState = 'validate';
|
| 306 |
this.skipFeedback = false;
|
| 307 |
this.pickedAtCheck.clear();
|
| 308 |
-
// or: this.pickedAtCheck = new Set<string>();
|
| 309 |
-
|
| 310 |
}
|
| 311 |
|
| 312 |
-
|
| 313 |
closeToMain(): void {
|
| 314 |
-
this.resetQuiz();
|
| 315 |
-
// If you also want to route away, you can add: this.router.navigate(['/home']);
|
| 316 |
}
|
| 317 |
-
|
| 318 |
}
|
|
|
|
| 1 |
import { Component, OnInit, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
|
| 2 |
import { VocabularyBuilderService } from './vocabulary-builder.service';
|
| 3 |
import { Router } from '@angular/router';
|
| 4 |
+
import { HeaderComponent } from '../shared/header/header.component';
|
| 5 |
+
import { ButtonComponent } from '../shared/button/button.component';
|
| 6 |
+
import { CommonModule } from '@angular/common';
|
| 7 |
+
import { FormsModule } from '@angular/forms';
|
| 8 |
|
| 9 |
@Component({
|
| 10 |
selector: 'app-vocabulary-builder',
|
| 11 |
templateUrl: './vocabulary-builder.component.html',
|
| 12 |
+
styleUrls: ['./vocabulary-builder.component.css'],
|
| 13 |
+
standalone: true,
|
| 14 |
+
imports: [ButtonComponent, CommonModule, FormsModule, HeaderComponent]
|
| 15 |
})
|
| 16 |
export class VocabularyBuilderComponent implements OnInit, AfterViewInit {
|
| 17 |
@ViewChild('step1', { static: false }) step1?: ElementRef;
|
|
|
|
| 93 |
|
| 94 |
const i = this.selectedOptions.indexOf(option);
|
| 95 |
if (i >= 0) {
|
|
|
|
| 96 |
this.selectedOptions.splice(i, 1);
|
| 97 |
} else {
|
|
|
|
| 98 |
if (this.selectedOptions.length >= 3) return;
|
| 99 |
this.selectedOptions.push(option);
|
| 100 |
}
|
| 101 |
|
|
|
|
| 102 |
this.isCheckButtonEnabled = this.selectedOptions.length === 3;
|
| 103 |
}
|
| 104 |
|
|
|
|
| 105 |
checkSelections(): void {
|
| 106 |
if (this.buttonState === 'next') {
|
| 107 |
this.step = 3;
|
|
|
|
| 119 |
this.processFeedback(response.feedback);
|
| 120 |
this.isChecked = true;
|
| 121 |
this.correctAnswers = response.correctAnswers || [];
|
|
|
|
| 122 |
this.pickedAtCheck = new Set(this.selectedOptions);
|
| 123 |
|
| 124 |
this.optionStates = {};
|
|
|
|
| 306 |
this.buttonState = 'validate';
|
| 307 |
this.skipFeedback = false;
|
| 308 |
this.pickedAtCheck.clear();
|
|
|
|
|
|
|
| 309 |
}
|
| 310 |
|
|
|
|
| 311 |
closeToMain(): void {
|
| 312 |
+
this.resetQuiz();
|
|
|
|
| 313 |
}
|
|
|
|
| 314 |
}
|
src/app/voice/voice.component.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
| 1 |
import { Component } from '@angular/core';
|
|
|
|
| 2 |
|
| 3 |
@Component({
|
| 4 |
selector: 'app-voice',
|
|
|
|
|
|
|
| 5 |
templateUrl: './voice.component.html',
|
| 6 |
styleUrls: ['./voice.component.css']
|
| 7 |
})
|
|
|
|
| 1 |
import { Component } from '@angular/core';
|
| 2 |
+
import { HeaderComponent } from '../shared/header/header.component';
|
| 3 |
|
| 4 |
@Component({
|
| 5 |
selector: 'app-voice',
|
| 6 |
+
standalone: true,
|
| 7 |
+
imports: [HeaderComponent],
|
| 8 |
templateUrl: './voice.component.html',
|
| 9 |
styleUrls: ['./voice.component.css']
|
| 10 |
})
|
src/app/writing/writing.component.css
CHANGED
|
@@ -1,5 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
body {
|
| 4 |
margin: 0;
|
| 5 |
font-family: 'Comic Sans MS', cursive, sans-serif;
|
|
@@ -7,12 +5,10 @@ body {
|
|
| 7 |
overflow: hidden;
|
| 8 |
}
|
| 9 |
|
| 10 |
-
/*main container */
|
| 11 |
-
|
| 12 |
.main-container {
|
| 13 |
width: 85%;
|
| 14 |
margin: 7vh auto;
|
| 15 |
-
border:
|
| 16 |
border-radius: 1.5vw;
|
| 17 |
background: #ffffff;
|
| 18 |
padding: 2vw;
|
|
@@ -20,9 +16,6 @@ body {
|
|
| 20 |
height: 73vh;
|
| 21 |
}
|
| 22 |
|
| 23 |
-
/* Section Layouts */
|
| 24 |
-
|
| 25 |
-
|
| 26 |
.writing-section {
|
| 27 |
display: flex;
|
| 28 |
flex-direction: row;
|
|
@@ -40,7 +33,7 @@ body {
|
|
| 40 |
}
|
| 41 |
|
| 42 |
.intro-left-image {
|
| 43 |
-
width: 35%;
|
| 44 |
}
|
| 45 |
|
| 46 |
.intro-left-image img {
|
|
@@ -48,8 +41,6 @@ body {
|
|
| 48 |
height: auto;
|
| 49 |
}
|
| 50 |
|
| 51 |
-
|
| 52 |
-
/* Left Image */
|
| 53 |
.left-image {
|
| 54 |
width: 25%;
|
| 55 |
}
|
|
@@ -58,10 +49,8 @@ body {
|
|
| 58 |
width: 100%;
|
| 59 |
}
|
| 60 |
|
| 61 |
-
/* Right Content */
|
| 62 |
.right-content {
|
| 63 |
width: 66%;
|
| 64 |
-
|
| 65 |
}
|
| 66 |
|
| 67 |
.right-content h2 {
|
|
@@ -72,18 +61,14 @@ body {
|
|
| 72 |
}
|
| 73 |
|
| 74 |
.right-content p {
|
| 75 |
-
|
| 76 |
font-size: 1.4vw;
|
| 77 |
-
/*margin-bottom: 30px;*/
|
| 78 |
text-align: justify;
|
| 79 |
}
|
| 80 |
|
| 81 |
-
/* Dropdown and Buttons */
|
| 82 |
.center-controls {
|
| 83 |
display: flex;
|
| 84 |
flex-direction: row;
|
| 85 |
align-items: center;
|
| 86 |
-
/*justify-content: center;*/
|
| 87 |
margin-top: 1vw;
|
| 88 |
gap: 1.5vw;
|
| 89 |
}
|
|
@@ -96,41 +81,12 @@ body {
|
|
| 96 |
|
| 97 |
.center-controls select {
|
| 98 |
font-size: 1.4vw;
|
| 99 |
-
padding: 0.
|
| 100 |
border-radius: 0.5vw;
|
| 101 |
border: 2px solid #009688;
|
| 102 |
background-color: #ffffff;
|
| 103 |
}
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
select,
|
| 108 |
-
button {
|
| 109 |
-
font-size: 1.2vw;
|
| 110 |
-
padding: 0.6vw 1.2vw;
|
| 111 |
-
border-radius: 0.5vw;
|
| 112 |
-
border: 2px solid #ccc;
|
| 113 |
-
margin-top: 1vw;
|
| 114 |
-
margin-right: 1vw;
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
button {
|
| 118 |
-
background-color: #006780;
|
| 119 |
-
color: white;
|
| 120 |
-
border: none;
|
| 121 |
-
cursor: pointer;
|
| 122 |
-
transition: background-color 0.3s;
|
| 123 |
-
margin-top: 1.5vw;
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
button:disabled {
|
| 127 |
-
background-color: #ccc !important;
|
| 128 |
-
color: #666 !important;
|
| 129 |
-
cursor: not-allowed !important;
|
| 130 |
-
opacity: 0.6;
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
/* Textarea */
|
| 134 |
textarea {
|
| 135 |
width: 100%;
|
| 136 |
height: 14vw;
|
|
@@ -145,10 +101,9 @@ textarea {
|
|
| 145 |
background-attachment: local;
|
| 146 |
}
|
| 147 |
|
| 148 |
-
/* Submit Area */
|
| 149 |
.submit-area {
|
| 150 |
text-align: center;
|
| 151 |
-
|
| 152 |
}
|
| 153 |
|
| 154 |
.submit-area button {
|
|
@@ -157,9 +112,6 @@ textarea {
|
|
| 157 |
border-radius: 1vw;
|
| 158 |
}
|
| 159 |
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
/* Loader Overlay */
|
| 163 |
.loader-overlay {
|
| 164 |
position: fixed;
|
| 165 |
top: 0;
|
|
@@ -217,13 +169,9 @@ textarea {
|
|
| 217 |
}
|
| 218 |
}
|
| 219 |
|
| 220 |
-
/*back arrow mark */
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
.back-icon {
|
| 225 |
position: absolute;
|
| 226 |
-
top:
|
| 227 |
left: 10vw;
|
| 228 |
color: #007bff;
|
| 229 |
font-size: 24px;
|
|
@@ -237,14 +185,12 @@ textarea {
|
|
| 237 |
color: #0056b3;
|
| 238 |
}
|
| 239 |
|
| 240 |
-
|
| 241 |
.back-icon img {
|
| 242 |
width: 30px;
|
| 243 |
height: auto;
|
| 244 |
cursor: pointer;
|
| 245 |
}
|
| 246 |
|
| 247 |
-
|
| 248 |
.topic-fieldset {
|
| 249 |
border: 2px solid #ffc107;
|
| 250 |
padding: 1vw;
|
|
@@ -271,9 +217,6 @@ textarea {
|
|
| 271 |
font-weight: 900;
|
| 272 |
}
|
| 273 |
|
| 274 |
-
/* feedback section*/
|
| 275 |
-
|
| 276 |
-
|
| 277 |
.feedback-section {
|
| 278 |
display: flex;
|
| 279 |
flex-direction: column;
|
|
@@ -307,8 +250,8 @@ textarea {
|
|
| 307 |
border: 2px dashed #ffca28;
|
| 308 |
border-radius: 1vw;
|
| 309 |
padding: 2vw 2vw 2vw 2vw;
|
| 310 |
-
width:
|
| 311 |
-
|
| 312 |
overflow-y: scroll;
|
| 313 |
}
|
| 314 |
|
|
@@ -326,12 +269,11 @@ textarea {
|
|
| 326 |
|
| 327 |
.try-again-button {
|
| 328 |
text-align: center;
|
|
|
|
| 329 |
}
|
| 330 |
|
| 331 |
.try-again-button button {
|
| 332 |
font-size: 1.3vw;
|
| 333 |
-
/*padding: 0.8vw 2vw;*/
|
| 334 |
-
|
| 335 |
border: none;
|
| 336 |
color: white;
|
| 337 |
border-radius: 1vw;
|
|
@@ -339,7 +281,6 @@ textarea {
|
|
| 339 |
transition: background-color 0.3s ease;
|
| 340 |
}
|
| 341 |
|
| 342 |
-
|
| 343 |
.animated {
|
| 344 |
animation: bounce 1s infinite;
|
| 345 |
}
|
|
@@ -354,8 +295,6 @@ textarea {
|
|
| 354 |
}
|
| 355 |
}
|
| 356 |
|
| 357 |
-
|
| 358 |
-
|
| 359 |
.with-blue-tooltip {
|
| 360 |
position: relative;
|
| 361 |
display: inline-block;
|
|
@@ -386,3 +325,8 @@ textarea {
|
|
| 386 |
border-style: solid;
|
| 387 |
border-color: transparent #2196f3 transparent transparent;
|
| 388 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
body {
|
| 2 |
margin: 0;
|
| 3 |
font-family: 'Comic Sans MS', cursive, sans-serif;
|
|
|
|
| 5 |
overflow: hidden;
|
| 6 |
}
|
| 7 |
|
|
|
|
|
|
|
| 8 |
.main-container {
|
| 9 |
width: 85%;
|
| 10 |
margin: 7vh auto;
|
| 11 |
+
border: 10px solid var(--main-accent-color);
|
| 12 |
border-radius: 1.5vw;
|
| 13 |
background: #ffffff;
|
| 14 |
padding: 2vw;
|
|
|
|
| 16 |
height: 73vh;
|
| 17 |
}
|
| 18 |
|
|
|
|
|
|
|
|
|
|
| 19 |
.writing-section {
|
| 20 |
display: flex;
|
| 21 |
flex-direction: row;
|
|
|
|
| 33 |
}
|
| 34 |
|
| 35 |
.intro-left-image {
|
| 36 |
+
width: 35%;
|
| 37 |
}
|
| 38 |
|
| 39 |
.intro-left-image img {
|
|
|
|
| 41 |
height: auto;
|
| 42 |
}
|
| 43 |
|
|
|
|
|
|
|
| 44 |
.left-image {
|
| 45 |
width: 25%;
|
| 46 |
}
|
|
|
|
| 49 |
width: 100%;
|
| 50 |
}
|
| 51 |
|
|
|
|
| 52 |
.right-content {
|
| 53 |
width: 66%;
|
|
|
|
| 54 |
}
|
| 55 |
|
| 56 |
.right-content h2 {
|
|
|
|
| 61 |
}
|
| 62 |
|
| 63 |
.right-content p {
|
|
|
|
| 64 |
font-size: 1.4vw;
|
|
|
|
| 65 |
text-align: justify;
|
| 66 |
}
|
| 67 |
|
|
|
|
| 68 |
.center-controls {
|
| 69 |
display: flex;
|
| 70 |
flex-direction: row;
|
| 71 |
align-items: center;
|
|
|
|
| 72 |
margin-top: 1vw;
|
| 73 |
gap: 1.5vw;
|
| 74 |
}
|
|
|
|
| 81 |
|
| 82 |
.center-controls select {
|
| 83 |
font-size: 1.4vw;
|
| 84 |
+
padding: 0.6vw;
|
| 85 |
border-radius: 0.5vw;
|
| 86 |
border: 2px solid #009688;
|
| 87 |
background-color: #ffffff;
|
| 88 |
}
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
textarea {
|
| 91 |
width: 100%;
|
| 92 |
height: 14vw;
|
|
|
|
| 101 |
background-attachment: local;
|
| 102 |
}
|
| 103 |
|
|
|
|
| 104 |
.submit-area {
|
| 105 |
text-align: center;
|
| 106 |
+
margin-top: 1vw;
|
| 107 |
}
|
| 108 |
|
| 109 |
.submit-area button {
|
|
|
|
| 112 |
border-radius: 1vw;
|
| 113 |
}
|
| 114 |
|
|
|
|
|
|
|
|
|
|
| 115 |
.loader-overlay {
|
| 116 |
position: fixed;
|
| 117 |
top: 0;
|
|
|
|
| 169 |
}
|
| 170 |
}
|
| 171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
.back-icon {
|
| 173 |
position: absolute;
|
| 174 |
+
top: 12vw;
|
| 175 |
left: 10vw;
|
| 176 |
color: #007bff;
|
| 177 |
font-size: 24px;
|
|
|
|
| 185 |
color: #0056b3;
|
| 186 |
}
|
| 187 |
|
|
|
|
| 188 |
.back-icon img {
|
| 189 |
width: 30px;
|
| 190 |
height: auto;
|
| 191 |
cursor: pointer;
|
| 192 |
}
|
| 193 |
|
|
|
|
| 194 |
.topic-fieldset {
|
| 195 |
border: 2px solid #ffc107;
|
| 196 |
padding: 1vw;
|
|
|
|
| 217 |
font-weight: 900;
|
| 218 |
}
|
| 219 |
|
|
|
|
|
|
|
|
|
|
| 220 |
.feedback-section {
|
| 221 |
display: flex;
|
| 222 |
flex-direction: column;
|
|
|
|
| 250 |
border: 2px dashed #ffca28;
|
| 251 |
border-radius: 1vw;
|
| 252 |
padding: 2vw 2vw 2vw 2vw;
|
| 253 |
+
width: 93%;
|
| 254 |
+
height: 19.5vw;
|
| 255 |
overflow-y: scroll;
|
| 256 |
}
|
| 257 |
|
|
|
|
| 269 |
|
| 270 |
.try-again-button {
|
| 271 |
text-align: center;
|
| 272 |
+
margin-top: 3vw;
|
| 273 |
}
|
| 274 |
|
| 275 |
.try-again-button button {
|
| 276 |
font-size: 1.3vw;
|
|
|
|
|
|
|
| 277 |
border: none;
|
| 278 |
color: white;
|
| 279 |
border-radius: 1vw;
|
|
|
|
| 281 |
transition: background-color 0.3s ease;
|
| 282 |
}
|
| 283 |
|
|
|
|
| 284 |
.animated {
|
| 285 |
animation: bounce 1s infinite;
|
| 286 |
}
|
|
|
|
| 295 |
}
|
| 296 |
}
|
| 297 |
|
|
|
|
|
|
|
| 298 |
.with-blue-tooltip {
|
| 299 |
position: relative;
|
| 300 |
display: inline-block;
|
|
|
|
| 325 |
border-style: solid;
|
| 326 |
border-color: transparent #2196f3 transparent transparent;
|
| 327 |
}
|
| 328 |
+
|
| 329 |
+
.user-guide-close-icon {
|
| 330 |
+
top: 9vw;
|
| 331 |
+
right: 7vw;
|
| 332 |
+
}
|
src/app/writing/writing.component.html
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
<!-- Header container -->
|
| 2 |
<app-header [title]="'Writing Exercise'"></app-header>
|
| 3 |
|
| 4 |
-
|
| 5 |
<!-- Background Image -->
|
| 6 |
<img src="assets/images/grammar-bg.png" alt="Chat Background" class="grammar-bg" />
|
| 7 |
|
|
@@ -32,15 +31,13 @@
|
|
| 32 |
<option value="middle">🧠 Middle</option>
|
| 33 |
<option value="upper">🎓 Upper</option>
|
| 34 |
</select>
|
| 35 |
-
<button
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
[class.loading]="isTopicFetching">
|
| 39 |
<span>Get Topic</span>
|
| 40 |
-
</button>
|
| 41 |
</div>
|
| 42 |
|
| 43 |
-
|
| 44 |
<div *ngIf="isLoaderVisible" class="loader-overlay">
|
| 45 |
<div class="loader"></div>
|
| 46 |
</div>
|
|
@@ -49,6 +46,7 @@
|
|
| 49 |
|
| 50 |
<!-- Writing Section -->
|
| 51 |
<div class="writing-section" *ngIf="showWriting">
|
|
|
|
| 52 |
<img src="assets/images/writing/back.png" alt="Go Back" class="back-icon" (click)="goBack('writing')" />
|
| 53 |
|
| 54 |
<div class="left-image">
|
|
@@ -61,21 +59,13 @@
|
|
| 61 |
</fieldset>
|
| 62 |
|
| 63 |
<textarea [(ngModel)]="userAnswer" placeholder="Write your answer here..." (input)="checkWordCount()"></textarea>
|
| 64 |
-
<!--<div class="submit-area">
|
| 65 |
-
<button (click)="submitAnswer()" [disabled]="isSubmitDisabled || isSubmitInProgress">
|
| 66 |
-
{{ isSubmitInProgress ? 'Submitting...' : 'Submit Writing' }}
|
| 67 |
-
</button>
|
| 68 |
-
<div *ngIf="isLoaderVisible" class="loader-overlay">
|
| 69 |
-
<div class="loader"></div>
|
| 70 |
-
</div>
|
| 71 |
-
</div>-->
|
| 72 |
|
| 73 |
<div class="submit-area with-blue-tooltip">
|
| 74 |
-
<button
|
| 75 |
-
|
| 76 |
-
|
| 77 |
{{ isSubmitInProgress ? 'Submitting...' : 'Submit Writing' }}
|
| 78 |
-
</button>
|
| 79 |
<div *ngIf="isLoaderVisible" class="loader-overlay">
|
| 80 |
<div class="loader"></div>
|
| 81 |
</div>
|
|
@@ -85,11 +75,9 @@
|
|
| 85 |
</div>
|
| 86 |
</div>
|
| 87 |
|
| 88 |
-
|
| 89 |
</div>
|
| 90 |
</div>
|
| 91 |
|
| 92 |
-
|
| 93 |
<!-- Feedback Section -->
|
| 94 |
<div class="feedback-section" *ngIf="showFeedback">
|
| 95 |
<img src="assets/images/writing/back.png" alt="Go Back" class="back-icon" (click)="goBack('feedback')" />
|
|
@@ -106,10 +94,8 @@
|
|
| 106 |
</div>
|
| 107 |
|
| 108 |
<div class="try-again-button">
|
| 109 |
-
<button (click)="resetForm()">🔄 Try Another</button>
|
| 110 |
</div>
|
| 111 |
</div>
|
| 112 |
|
| 113 |
-
|
| 114 |
-
|
| 115 |
</div>
|
|
|
|
| 1 |
<!-- Header container -->
|
| 2 |
<app-header [title]="'Writing Exercise'"></app-header>
|
| 3 |
|
|
|
|
| 4 |
<!-- Background Image -->
|
| 5 |
<img src="assets/images/grammar-bg.png" alt="Chat Background" class="grammar-bg" />
|
| 6 |
|
|
|
|
| 31 |
<option value="middle">🧠 Middle</option>
|
| 32 |
<option value="upper">🎓 Upper</option>
|
| 33 |
</select>
|
| 34 |
+
<app-button [disabled]="isTopicFetching"
|
| 35 |
+
[ngClass]="{ loading: isTopicFetching }"
|
| 36 |
+
(click)="fetchTopic()">
|
|
|
|
| 37 |
<span>Get Topic</span>
|
| 38 |
+
</app-button>
|
| 39 |
</div>
|
| 40 |
|
|
|
|
| 41 |
<div *ngIf="isLoaderVisible" class="loader-overlay">
|
| 42 |
<div class="loader"></div>
|
| 43 |
</div>
|
|
|
|
| 46 |
|
| 47 |
<!-- Writing Section -->
|
| 48 |
<div class="writing-section" *ngIf="showWriting">
|
| 49 |
+
<button class="user-guide-close-icon" (click)="goBack('writing')">×</button>
|
| 50 |
<img src="assets/images/writing/back.png" alt="Go Back" class="back-icon" (click)="goBack('writing')" />
|
| 51 |
|
| 52 |
<div class="left-image">
|
|
|
|
| 59 |
</fieldset>
|
| 60 |
|
| 61 |
<textarea [(ngModel)]="userAnswer" placeholder="Write your answer here..." (input)="checkWordCount()"></textarea>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
<div class="submit-area with-blue-tooltip">
|
| 64 |
+
<app-button [disabled]="isSubmitDisabled || isSubmitInProgress"
|
| 65 |
+
[ngClass]="{ 'disabled-btn': isSubmitDisabled }"
|
| 66 |
+
(click)="submitAnswer()">
|
| 67 |
{{ isSubmitInProgress ? 'Submitting...' : 'Submit Writing' }}
|
| 68 |
+
</app-button>
|
| 69 |
<div *ngIf="isLoaderVisible" class="loader-overlay">
|
| 70 |
<div class="loader"></div>
|
| 71 |
</div>
|
|
|
|
| 75 |
</div>
|
| 76 |
</div>
|
| 77 |
|
|
|
|
| 78 |
</div>
|
| 79 |
</div>
|
| 80 |
|
|
|
|
| 81 |
<!-- Feedback Section -->
|
| 82 |
<div class="feedback-section" *ngIf="showFeedback">
|
| 83 |
<img src="assets/images/writing/back.png" alt="Go Back" class="back-icon" (click)="goBack('feedback')" />
|
|
|
|
| 94 |
</div>
|
| 95 |
|
| 96 |
<div class="try-again-button">
|
| 97 |
+
<app-button (click)="resetForm()">🔄 Try Another</app-button>
|
| 98 |
</div>
|
| 99 |
</div>
|
| 100 |
|
|
|
|
|
|
|
| 101 |
</div>
|
src/app/writing/writing.component.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
| 1 |
import { Component } from '@angular/core';
|
| 2 |
import { WritingService } from './writing.service';
|
| 3 |
import { Router } from '@angular/router';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
@Component({
|
| 6 |
selector: 'app-writing',
|
| 7 |
templateUrl: './writing.component.html',
|
| 8 |
-
styleUrls: ['./writing.component.css']
|
|
|
|
|
|
|
| 9 |
})
|
| 10 |
export class WritingComponent {
|
| 11 |
gradeLevel = 'lower';
|
|
@@ -15,9 +21,9 @@ export class WritingComponent {
|
|
| 15 |
feedback: string | null = null;
|
| 16 |
isFeedbackVisible: boolean = false;
|
| 17 |
isSubmitDisabled: boolean = true;
|
| 18 |
-
isTopicFetching: boolean = false;
|
| 19 |
-
isLoading: boolean = false;
|
| 20 |
-
isSubmitInProgress: boolean = false;
|
| 21 |
feedbackList: string[] = [];
|
| 22 |
showStarAnimation: boolean = false;
|
| 23 |
isLoaderVisible: boolean = false;
|
|
@@ -27,7 +33,7 @@ export class WritingComponent {
|
|
| 27 |
showFeedback: boolean = false;
|
| 28 |
|
| 29 |
constructor(private writingService: WritingService, private router: Router) { }
|
| 30 |
-
|
| 31 |
fetchTopic(): void {
|
| 32 |
this.isLoaderVisible = true;
|
| 33 |
this.isTopicFetching = true;
|
|
@@ -37,7 +43,7 @@ export class WritingComponent {
|
|
| 37 |
this.showWriting = false;
|
| 38 |
this.showIntro = true;
|
| 39 |
this.showFeedback = false;
|
| 40 |
-
this.userAnswer = '';
|
| 41 |
|
| 42 |
this.writingService.getWritingTopic(this.gradeLevel).subscribe(
|
| 43 |
(response) => {
|
|
@@ -63,7 +69,6 @@ export class WritingComponent {
|
|
| 63 |
);
|
| 64 |
}
|
| 65 |
|
| 66 |
-
|
| 67 |
submitAnswer(): void {
|
| 68 |
if (this.userAnswer.trim() === '') {
|
| 69 |
this.errorMessage = 'Please write an answer before submitting.';
|
|
@@ -79,9 +84,8 @@ export class WritingComponent {
|
|
| 79 |
this.isSubmitInProgress = true;
|
| 80 |
this.isLoaderVisible = true;
|
| 81 |
|
| 82 |
-
// Don't show feedback screen immediately – wait for response
|
| 83 |
this.showIntro = false;
|
| 84 |
-
this.showWriting = true;
|
| 85 |
this.showFeedback = false;
|
| 86 |
|
| 87 |
this.writingService.validateResponse(this.topic, this.userAnswer).subscribe({
|
|
@@ -94,7 +98,6 @@ export class WritingComponent {
|
|
| 94 |
this.isSubmitInProgress = false;
|
| 95 |
this.isLoaderVisible = false;
|
| 96 |
|
| 97 |
-
// ✅ Now move to feedback after getting response
|
| 98 |
this.showWriting = false;
|
| 99 |
this.showFeedback = true;
|
| 100 |
},
|
|
@@ -110,29 +113,23 @@ export class WritingComponent {
|
|
| 110 |
});
|
| 111 |
}
|
| 112 |
|
| 113 |
-
|
| 114 |
checkWordCount(): void {
|
| 115 |
setTimeout(() => {
|
| 116 |
const wordCount = this.userAnswer.trim().split(/\s+/).length;
|
| 117 |
-
this.isSubmitDisabled = wordCount < 10;
|
| 118 |
});
|
| 119 |
}
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
resetForm(): void {
|
| 126 |
-
this.gradeLevel = 'lower';
|
| 127 |
-
this.topic = null;
|
| 128 |
-
this.userAnswer = '';
|
| 129 |
-
this.feedback = null;
|
| 130 |
-
this.errorMessage = null;
|
| 131 |
-
this.isFeedbackVisible = false;
|
| 132 |
-
this.isSubmitDisabled = true;
|
| 133 |
-
this.isTopicFetching = false;
|
| 134 |
-
this.isSubmitInProgress = false;
|
| 135 |
-
// UI flags
|
| 136 |
this.showIntro = true;
|
| 137 |
this.showWriting = false;
|
| 138 |
this.showFeedback = false;
|
|
@@ -140,11 +137,9 @@ export class WritingComponent {
|
|
| 140 |
|
| 141 |
goBack(from: string): void {
|
| 142 |
if (from === 'writing') {
|
| 143 |
-
// Go back to Intro
|
| 144 |
this.showWriting = false;
|
| 145 |
this.showIntro = true;
|
| 146 |
} else if (from === 'feedback') {
|
| 147 |
-
// Go back to Writing
|
| 148 |
this.showFeedback = false;
|
| 149 |
this.showWriting = true;
|
| 150 |
}
|
|
@@ -157,7 +152,6 @@ export class WritingComponent {
|
|
| 157 |
goToListening(): void {
|
| 158 |
this.router.navigate(['/listening']);
|
| 159 |
}
|
| 160 |
-
|
| 161 |
}
|
| 162 |
|
| 163 |
|
|
|
|
| 1 |
import { Component } from '@angular/core';
|
| 2 |
import { WritingService } from './writing.service';
|
| 3 |
import { Router } from '@angular/router';
|
| 4 |
+
import { HeaderComponent } from '../shared/header/header.component';
|
| 5 |
+
import { ButtonComponent } from '../shared/button/button.component';
|
| 6 |
+
import { CommonModule } from '@angular/common';
|
| 7 |
+
import { FormsModule } from '@angular/forms';
|
| 8 |
|
| 9 |
@Component({
|
| 10 |
selector: 'app-writing',
|
| 11 |
templateUrl: './writing.component.html',
|
| 12 |
+
styleUrls: ['./writing.component.css'],
|
| 13 |
+
standalone: true,
|
| 14 |
+
imports: [ButtonComponent, CommonModule, FormsModule, HeaderComponent]
|
| 15 |
})
|
| 16 |
export class WritingComponent {
|
| 17 |
gradeLevel = 'lower';
|
|
|
|
| 21 |
feedback: string | null = null;
|
| 22 |
isFeedbackVisible: boolean = false;
|
| 23 |
isSubmitDisabled: boolean = true;
|
| 24 |
+
isTopicFetching: boolean = false;
|
| 25 |
+
isLoading: boolean = false;
|
| 26 |
+
isSubmitInProgress: boolean = false;
|
| 27 |
feedbackList: string[] = [];
|
| 28 |
showStarAnimation: boolean = false;
|
| 29 |
isLoaderVisible: boolean = false;
|
|
|
|
| 33 |
showFeedback: boolean = false;
|
| 34 |
|
| 35 |
constructor(private writingService: WritingService, private router: Router) { }
|
| 36 |
+
|
| 37 |
fetchTopic(): void {
|
| 38 |
this.isLoaderVisible = true;
|
| 39 |
this.isTopicFetching = true;
|
|
|
|
| 43 |
this.showWriting = false;
|
| 44 |
this.showIntro = true;
|
| 45 |
this.showFeedback = false;
|
| 46 |
+
this.userAnswer = '';
|
| 47 |
|
| 48 |
this.writingService.getWritingTopic(this.gradeLevel).subscribe(
|
| 49 |
(response) => {
|
|
|
|
| 69 |
);
|
| 70 |
}
|
| 71 |
|
|
|
|
| 72 |
submitAnswer(): void {
|
| 73 |
if (this.userAnswer.trim() === '') {
|
| 74 |
this.errorMessage = 'Please write an answer before submitting.';
|
|
|
|
| 84 |
this.isSubmitInProgress = true;
|
| 85 |
this.isLoaderVisible = true;
|
| 86 |
|
|
|
|
| 87 |
this.showIntro = false;
|
| 88 |
+
this.showWriting = true;
|
| 89 |
this.showFeedback = false;
|
| 90 |
|
| 91 |
this.writingService.validateResponse(this.topic, this.userAnswer).subscribe({
|
|
|
|
| 98 |
this.isSubmitInProgress = false;
|
| 99 |
this.isLoaderVisible = false;
|
| 100 |
|
|
|
|
| 101 |
this.showWriting = false;
|
| 102 |
this.showFeedback = true;
|
| 103 |
},
|
|
|
|
| 113 |
});
|
| 114 |
}
|
| 115 |
|
|
|
|
| 116 |
checkWordCount(): void {
|
| 117 |
setTimeout(() => {
|
| 118 |
const wordCount = this.userAnswer.trim().split(/\s+/).length;
|
| 119 |
+
this.isSubmitDisabled = wordCount < 10;
|
| 120 |
});
|
| 121 |
}
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
resetForm(): void {
|
| 124 |
+
this.gradeLevel = 'lower';
|
| 125 |
+
this.topic = null;
|
| 126 |
+
this.userAnswer = '';
|
| 127 |
+
this.feedback = null;
|
| 128 |
+
this.errorMessage = null;
|
| 129 |
+
this.isFeedbackVisible = false;
|
| 130 |
+
this.isSubmitDisabled = true;
|
| 131 |
+
this.isTopicFetching = false;
|
| 132 |
+
this.isSubmitInProgress = false;
|
|
|
|
| 133 |
this.showIntro = true;
|
| 134 |
this.showWriting = false;
|
| 135 |
this.showFeedback = false;
|
|
|
|
| 137 |
|
| 138 |
goBack(from: string): void {
|
| 139 |
if (from === 'writing') {
|
|
|
|
| 140 |
this.showWriting = false;
|
| 141 |
this.showIntro = true;
|
| 142 |
} else if (from === 'feedback') {
|
|
|
|
| 143 |
this.showFeedback = false;
|
| 144 |
this.showWriting = true;
|
| 145 |
}
|
|
|
|
| 152 |
goToListening(): void {
|
| 153 |
this.router.navigate(['/listening']);
|
| 154 |
}
|
|
|
|
| 155 |
}
|
| 156 |
|
| 157 |
|