guess-the-year-web-component 3.2.3 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +2 -2
  2. package/custom-elements.json +19 -1
  3. package/dist/guess-the-year.d.ts +21 -16
  4. package/dist/guess-the-year.d.ts.map +1 -1
  5. package/dist/index.html +3 -8
  6. package/dist/lib/controller/scorecontroller.d.ts +2 -1
  7. package/dist/lib/controller/scorecontroller.d.ts.map +1 -1
  8. package/dist/lib/i18n/EN.d.ts.map +1 -1
  9. package/dist/lib/i18n/NL.d.ts.map +1 -1
  10. package/dist/main.bundle.js +200 -0
  11. package/dist/main.bundle.js.map +1 -0
  12. package/package.json +26 -27
  13. package/src/guess-the-year.ts +335 -273
  14. package/src/index.html +2 -7
  15. package/src/lib/ApiService.ts +1 -1
  16. package/src/lib/Util.ts +1 -1
  17. package/src/lib/controller/scorecontroller.ts +12 -1
  18. package/src/lib/i18n/EN.ts +18 -4
  19. package/src/lib/i18n/NL.ts +19 -5
  20. package/src/scss/_layout.scss +18 -5
  21. package/src/scss/_typography.scss +3 -2
  22. package/src/scss/_variables.scss +5 -5
  23. package/src/scss/main.scss +55 -1
  24. package/src/scss/views/_intro.scss +16 -4
  25. package/src/scss/views/_stage.scss +149 -43
  26. package/webpack.config.cjs +23 -25
  27. package/dist/guess-the-year.js +0 -751
  28. package/dist/guess-the-year.js.map +0 -1
  29. package/dist/lib/ApiService.js +0 -134
  30. package/dist/lib/ApiService.js.map +0 -1
  31. package/dist/lib/Icon.js +0 -2
  32. package/dist/lib/Icon.js.map +0 -1
  33. package/dist/lib/Incident.js +0 -37
  34. package/dist/lib/Incident.js.map +0 -1
  35. package/dist/lib/Util.js +0 -69
  36. package/dist/lib/Util.js.map +0 -1
  37. package/dist/lib/controller/scorecontroller.js +0 -38
  38. package/dist/lib/controller/scorecontroller.js.map +0 -1
  39. package/dist/lib/enums.js +0 -10
  40. package/dist/lib/enums.js.map +0 -1
  41. package/dist/lib/i18n/EN.js +0 -148
  42. package/dist/lib/i18n/EN.js.map +0 -1
  43. package/dist/lib/i18n/NL.js +0 -152
  44. package/dist/lib/i18n/NL.js.map +0 -1
  45. package/dist/lib/i18n/i18n.js +0 -10
  46. package/dist/lib/i18n/i18n.js.map +0 -1
  47. package/dist/main.js +0 -205
  48. package/todo.txt +0 -0
  49. /package/dist/{main.js.LICENSE.txt → main.bundle.js.LICENSE.txt} +0 -0
@@ -3,7 +3,7 @@ import { customElement, property, state } from "lit/decorators.js";
3
3
  import dayjs from "dayjs";
4
4
  import { Dayjs } from "dayjs";
5
5
  const { setTimeout } = window;
6
- import { Incident, Categories, Category } from "./lib/Incident";
6
+ import { Incident } from "./lib/Incident";
7
7
  import { NoImageImg } from "./lib/Icon";
8
8
  import { ApiService, ContentFeedback } from "./lib/ApiService";
9
9
  import { Util } from "./lib/Util";
@@ -14,7 +14,6 @@ import { QuestionScoreController } from "./lib/controller/scorecontroller";
14
14
 
15
15
  const SPLASH_DELAY: number = 1000;
16
16
  const MAX_QUESTIONS: number = 10;
17
- const HINT_COST: number = 2;
18
17
  const MIN_YEAR: number = 1950;
19
18
  const MAX_YEAR: number = parseInt(dayjs().format('YYYY'), 10);
20
19
  const GTY_FROM_YEAR_VAR = 'gty-from-year';
@@ -23,6 +22,8 @@ const GTY_NUM_QUESTIONS_VAR = 'gty-number-of-questions';
23
22
  const MAX_DOB: number = 1915;
24
23
  const MIN_DOB: number = parseInt(dayjs().format('YYYY'), 10) - 10;
25
24
  const GTY_PLAYER_DOB_VAR = 'gty-player-dob';
25
+ const GTY_TIME_PER_QUESTION_VAR = 'gty-time-per-question';
26
+ const DEFAULT_TIME_PER_QUESTION: number = 10;
26
27
 
27
28
  @customElement("guess-the-year")
28
29
  export class GuessTheYear extends LitElement {
@@ -113,42 +114,48 @@ export class GuessTheYear extends LitElement {
113
114
  @property({ type: Boolean, attribute: "suppress-images" })
114
115
  suppressImages: boolean = false;
115
116
 
116
- @property({ type: Number, attribute: "max-hints" })
117
- maxHints: number = 3;
118
-
119
117
  @property({ type: Number, attribute: "number-of-questions", reflect: true })
120
118
  numberOfQuestions: number = parseInt(window.localStorage.getItem(GTY_NUM_QUESTIONS_VAR) || MAX_QUESTIONS + '', 10);
121
119
  private _numberOfQuestionsSeen: number = 0;
122
120
 
123
121
  private _playerDob: number = parseInt(window.localStorage.getItem(GTY_PLAYER_DOB_VAR) || 0 + '', 10);
124
122
 
125
- @state()
126
- private _incident: Incident | undefined = undefined;
127
-
128
- private _answers: Array<Number> = [];
123
+ @property({ type: Number, attribute: "time-per-question" })
124
+ timePerQuestion: number = parseInt(window.localStorage.getItem(GTY_TIME_PER_QUESTION_VAR) || DEFAULT_TIME_PER_QUESTION + '', 10);
129
125
 
130
126
  @state()
131
- private _hint: string | undefined = undefined;
127
+ private _incident: Incident | undefined = undefined;
132
128
 
133
- private _showHint: boolean = true;
134
129
  private _showFooter: boolean = false;
135
- private _hintsIndex: number = 0;
136
- private _hints: Array<Incident> = [];
137
130
 
138
131
  @state()
139
- private _solution: Number | undefined = undefined;
132
+ private _solution: number | undefined = undefined;
140
133
 
141
134
  @state()
142
135
  public _renderState: string = GameState.INIT;
143
136
 
144
- @state()
145
- private _score = 30;
146
-
147
137
  public totalScore = 0;
148
138
 
149
- // _answer is stored as string because it will be entered character for character
139
+ // Timeline state
140
+ @state()
141
+ private _timelineYears: number[] = [];
142
+
150
143
  @state()
151
- private _answer: number | undefined = undefined;
144
+ private _clickedPosition: number | undefined = undefined;
145
+
146
+ @state()
147
+ private _correctYear: number | undefined = undefined;
148
+
149
+ private _scoreAddedForCurrentQuestion: boolean = false;
150
+ private _userClicked: boolean = false; // Track if user actually clicked (vs timeout)
151
+
152
+ @state()
153
+ private _toastMessage: string | undefined = undefined;
154
+
155
+ @state()
156
+ private _toastType: 'correct' | 'wrong' | undefined = undefined;
157
+
158
+ private _toastTimeout: number | undefined = undefined;
152
159
 
153
160
  private _apiService = new ApiService(this.teeeApiUrl);
154
161
  private i18n = i18nFactory("nl");
@@ -159,7 +166,23 @@ export class GuessTheYear extends LitElement {
159
166
 
160
167
  constructor() {
161
168
  super();
162
- this._score0 = new QuestionScoreController(this, 30, 1000);
169
+ // Initialize with default, will be reset with actual timePerQuestion when needed
170
+ this._score0 = new QuestionScoreController(this, DEFAULT_TIME_PER_QUESTION, 1000, () => {
171
+ this._handleTimeUp();
172
+ });
173
+ }
174
+
175
+ // Handles when time runs out - treat as wrong answer
176
+ private _handleTimeUp(): void {
177
+ // If user hasn't clicked yet, treat as wrong answer
178
+ if (!this._userClicked) {
179
+ // Don't set _clickedPosition - timeout is always wrong
180
+ // Process feedback (will be wrong since no answer was given)
181
+ this._processFeedback();
182
+
183
+ // Request update to show feedback
184
+ this.requestUpdate();
185
+ }
163
186
  }
164
187
 
165
188
  override connectedCallback() {
@@ -215,6 +238,7 @@ export class GuessTheYear extends LitElement {
215
238
  ${this._renderState == GameState.PLAY ? this.renderGameStage() : ""}
216
239
  ${this._renderState == GameState.END ? this.renderGameEnd() : ""}
217
240
  ${this._renderFooter()}
241
+ ${this._renderToast()}
218
242
  </div>
219
243
  `;
220
244
  }
@@ -224,13 +248,9 @@ export class GuessTheYear extends LitElement {
224
248
  this?.shadowRoot?.querySelector(element)?.style.setProperty("color", color);
225
249
  }
226
250
 
227
- // this renders the game header
228
- private _renderHeader(content: string = this.i18n.translate("game-name")): TemplateResult {
229
- return html`
230
- <section id="top" class="header guess-the-year-header">
231
- <h1>${content}</h1>
232
- </section>
233
- `;
251
+ // this renders the game header (disabled for tile theme)
252
+ private _renderHeader(_content: string = this.i18n.translate("game-name")): TemplateResult {
253
+ return html``;
234
254
  }
235
255
 
236
256
  // this renders the game footer
@@ -300,6 +320,17 @@ export class GuessTheYear extends LitElement {
300
320
  `);
301
321
  }
302
322
 
323
+ // pre-render time per question options
324
+ const timeOptions = [2, 5, 10, 15];
325
+ const selectTimeOptions = [];
326
+ for (const time of timeOptions) {
327
+ selectTimeOptions.push(html`
328
+ <option value="${time}" ?selected=${this.timePerQuestion == time}>
329
+ ${time}
330
+ </option>
331
+ `);
332
+ }
333
+
303
334
  return html`
304
335
  ${this._renderHeader(this.i18n.translate("game-introduction-welcome"))}
305
336
 
@@ -307,15 +338,8 @@ export class GuessTheYear extends LitElement {
307
338
  <div class="game-explanation">
308
339
  <p>
309
340
  ${this.i18n.translate("game-introduction-p1")}
310
- </p>
311
-
312
- <p>
313
341
  ${this.i18n.translate("game-introduction-p2")}
314
342
  </p>
315
-
316
- <p>
317
- ${this.i18n.translate("game-introduction-p3")}
318
- </p>
319
343
  </div>
320
344
 
321
345
  <div class="game-settings">
@@ -327,6 +351,11 @@ export class GuessTheYear extends LitElement {
327
351
  ${selectQuestionOptions}
328
352
  </select>
329
353
  ${this.i18n.translate("game-settings-p1-2")}
354
+ ${this.i18n.translate("game-settings-time-1")}
355
+ <select @change="${this._setTimePerQuestion}">
356
+ ${selectTimeOptions}
357
+ </select>
358
+ ${this.i18n.translate("game-settings-time-2")}
330
359
  ${this.i18n.translate("game-settings-p2")}
331
360
  <select @change="${this._setFromDate}">
332
361
  ${this._renderYearStepXSelectOptions(MIN_YEAR, MAX_YEAR, 5, this.from.format("YYYY"))}
@@ -360,6 +389,11 @@ export class GuessTheYear extends LitElement {
360
389
  window.localStorage.setItem(GTY_NUM_QUESTIONS_VAR, this.numberOfQuestions + '');
361
390
  }
362
391
 
392
+ private _setTimePerQuestion(event: any): void {
393
+ this.timePerQuestion = Number(event.target.value);
394
+ window.localStorage.setItem(GTY_TIME_PER_QUESTION_VAR, this.timePerQuestion + '');
395
+ }
396
+
363
397
  private _setFromDate(event: any): void {
364
398
  this.from = this.from.year(event.target.value);
365
399
  window.localStorage.setItem(GTY_FROM_YEAR_VAR, this.from.format('YYYY') + '');
@@ -381,16 +415,12 @@ export class GuessTheYear extends LitElement {
381
415
 
382
416
  <div class="container">
383
417
  <div class="incident">${this._renderIncident()}</div>
384
- ${this._renderHint()}
385
418
  <div class="sources">${this.renderSources()}</div>
386
419
  </div>
387
420
 
388
421
  <div class="game-controls">
389
- <div class="game-controls--hint">${this._renderHintButton()}</div>
390
- <div class="game-controls--answers">${this._renderAnswers()}</div>
422
+ <div class="game-controls--timeline">${this._renderTimeline()}</div>
391
423
  </div>
392
-
393
- ${this._renderFeedback()}
394
424
  </div>
395
425
  `;
396
426
  }
@@ -441,23 +471,82 @@ export class GuessTheYear extends LitElement {
441
471
  return html``;
442
472
  }
443
473
 
444
- // renders a list of possible answers
445
- private _renderAnswers(): TemplateResult {
474
+ // renders the timeline for relative year positioning
475
+ private _renderTimeline(): TemplateResult {
476
+ const fromYear = this.from.year();
477
+ const toYear = this.to.year();
478
+ const timelineRange = toYear - fromYear;
479
+
480
+ // Calculate positions for known years on timeline (0-100%)
481
+ const yearPositions = this._timelineYears.map(year => {
482
+ const position = ((year - fromYear) / timelineRange) * 100;
483
+ return { year, position: Math.max(0, Math.min(100, position)) };
484
+ });
485
+
486
+ // Calculate clicked position if available
487
+ let clickedPositionPercent: number | undefined = undefined;
488
+ if (this._clickedPosition !== undefined) {
489
+ clickedPositionPercent = ((this._clickedPosition - fromYear) / timelineRange) * 100;
490
+ clickedPositionPercent = Math.max(0, Math.min(100, clickedPositionPercent));
491
+ }
492
+
493
+ // Calculate correct position if available
494
+ let correctPositionPercent: number | undefined = undefined;
495
+ if (this._correctYear !== undefined) {
496
+ correctPositionPercent = ((this._correctYear - fromYear) / timelineRange) * 100;
497
+ correctPositionPercent = Math.max(0, Math.min(100, correctPositionPercent));
498
+ }
499
+
446
500
  return html`
447
- <div class="answers--list">
448
- <ul>
449
- ${this._answers.map(
450
- (answer) =>
451
- html`<li @click="${this._handleAnswerClicked}">${answer}</li>`,
452
- )}
453
- </ul>
501
+ <div class="timeline-container">
502
+ <div
503
+ class="timeline"
504
+ @click="${this._handleTimelineClick}"
505
+ >
506
+ <!-- Timeline line -->
507
+ <div class="timeline-line"></div>
508
+
509
+ <!-- Year markers (tegeltjes) -->
510
+ ${yearPositions.map(({ year, position }) => html`
511
+ <div
512
+ class="timeline-tile"
513
+ style="left: ${position}%;"
514
+ >
515
+ <div class="timeline-tile-content">
516
+ ${year}
517
+ </div>
518
+ </div>
519
+ `)}
520
+
521
+ <!-- Clicked position indicator -->
522
+ ${clickedPositionPercent !== undefined ? html`
523
+ <div
524
+ class="timeline-clicked ${this._isTimelinePositionCorrect() ? 'timeline-clicked-correct' : 'timeline-clicked-wrong'}"
525
+ style="left: ${clickedPositionPercent}%;"
526
+ ></div>
527
+ ` : ''}
528
+
529
+ <!-- Correct position indicator (shown after answer) -->
530
+ ${correctPositionPercent !== undefined && this._clickedPosition !== undefined ? html`
531
+ <div
532
+ class="timeline-correct"
533
+ style="left: ${correctPositionPercent}%;"
534
+ ></div>
535
+ ` : ''}
536
+
537
+ <!-- Year labels at start and end -->
538
+ <div class="timeline-year-start">${fromYear}</div>
539
+ <div class="timeline-year-end">${toYear}</div>
540
+ </div>
454
541
  </div>
455
542
  `;
456
543
  }
457
544
 
458
545
  // renders the current question score
459
- // it counts back to 0
460
546
  private _renderGameScore(): TemplateResult {
547
+ // Show the countdown score (remaining time/points)
548
+ const currentQuestionScore = Math.max(0, this._score0.value);
549
+
461
550
  return html`
462
551
  <div class="game-score--total">
463
552
  <div class="game-score--label">${this.i18n.translate("question-number")}:</div>
@@ -468,7 +557,7 @@ export class GuessTheYear extends LitElement {
468
557
 
469
558
  <div class="game-score--score">
470
559
  <div class="game-score--label">${this.i18n.translate("question-score")}:</div>
471
- <div class="game-score--value">${this._score0.value}</div>
560
+ <div class="game-score--value">${currentQuestionScore}</div>
472
561
  </div>
473
562
 
474
563
  <div class="game-score--total">
@@ -478,157 +567,208 @@ export class GuessTheYear extends LitElement {
478
567
  `;
479
568
  }
480
569
 
481
- // handles the click on an answer
482
- private _handleAnswerClicked(event: any): void {
483
- // get the clicked answer
484
- const answer = event.target.textContent;
570
+ // handles the click on the timeline
571
+ private _handleTimelineClick(event: MouseEvent): void {
572
+ if (!this._correctYear) {
573
+ return; // No question loaded yet
574
+ }
485
575
 
486
- // stop the score counter
576
+ const timelineElement = event.currentTarget as HTMLElement;
577
+ const rect = timelineElement.getBoundingClientRect();
578
+ const clickX = event.clientX - rect.left;
579
+ const clickPercentage = (clickX / rect.width) * 100;
580
+
581
+ const fromYear = this.from.year();
582
+ const toYear = this.to.year();
583
+ const timelineRange = toYear - fromYear;
584
+
585
+ // Convert click position to year
586
+ const clickedYear = Math.round(fromYear + (clickPercentage / 100) * timelineRange);
587
+
588
+ // Clamp to valid range
589
+ const clampedYear = Math.max(fromYear, Math.min(toYear, clickedYear));
590
+
591
+ this._clickedPosition = clampedYear;
592
+ this._userClicked = true; // Mark that user actually clicked
593
+
594
+ // Stop the score counter (if still running)
487
595
  this._score0.stop();
488
596
 
489
- // remove the hint button
490
- this._showHint = false;
491
-
492
- // check if the answer is correct
493
- if (answer) {
494
- // this._apiService.postIncidentAnswer(this._incident?.id, answer);
495
- console.debug(
496
- answer,
497
- this._solution,
498
- this._score,
499
- answer == this._solution,
500
- );
501
-
502
- this._answer = answer;
503
- }
597
+ // Process feedback and show toast
598
+ this._processFeedback();
599
+
600
+ // Request update to show feedback
601
+ this.requestUpdate();
504
602
  }
505
603
 
506
- // renders the requested hint
507
- private _renderHint(): TemplateResult {
508
- if (!this._hint) {
509
- return html``;
604
+ // checks if the clicked position is correct (between older and younger events)
605
+ private _isTimelinePositionCorrect(): boolean {
606
+ if (this._clickedPosition === undefined || this._correctYear === undefined) {
607
+ return false;
510
608
  }
511
609
 
512
- return html` <p>${this._hint}</p> `;
610
+ // Find the position where the correct year should be inserted
611
+ const sortedKnownYears = [...this._timelineYears].sort((a, b) => a - b);
612
+
613
+ // Find the two years that bracket the correct year
614
+ let lowerBound: number | null = null;
615
+ let upperBound: number | null = null;
616
+
617
+ for (let i = 0; i < sortedKnownYears.length; i++) {
618
+ if (sortedKnownYears[i] < this._correctYear) {
619
+ lowerBound = sortedKnownYears[i];
620
+ } else if (sortedKnownYears[i] > this._correctYear && upperBound === null) {
621
+ upperBound = sortedKnownYears[i];
622
+ break;
623
+ }
624
+ }
625
+
626
+ // If no lower bound, use the start of the timeline
627
+ if (lowerBound === null) {
628
+ lowerBound = this.from.year();
629
+ }
630
+
631
+ // If no upper bound, use the end of the timeline
632
+ if (upperBound === null) {
633
+ upperBound = this.to.year();
634
+ }
635
+
636
+ // Check if clicked position is between lower and upper bound
637
+ return this._clickedPosition >= lowerBound && this._clickedPosition <= upperBound;
513
638
  }
514
639
 
515
- // renders the hint button until
516
- // all max hints have been used
517
- private _renderHintButton(): TemplateResult {
518
- if (!this._showHint) {
519
- return html``;
520
- }
521
640
 
522
- return html`
523
- <div class="game-controls--hint">
524
- <button @click="${this._handleHintClicked}">
525
- ${this.i18n.translate("show-hint")} (${this._hints.length - this._hintsIndex})
526
- </button>
527
- </div>
528
- `;
529
- }
641
+ /**
642
+ * Processes feedback and shows toast notification
643
+ */
644
+ private _processFeedback(): void {
645
+ // Stop score counter if still running
646
+ if (this._score0.isRunning()) {
647
+ this._score0.stop();
648
+ }
530
649
 
531
- // handles the click on the hint button
532
- // and shows the next hint if available
533
- // subtracts the cost of the hint from
534
- // the question score
535
- private _handleHintClicked(): void {
536
- console.debug("handleHintClicked", [this._hintsIndex, this._hints.length]);
650
+ // If user didn't click (timeout), always treat as wrong
651
+ if (!this._userClicked) {
652
+ const is_correct = false;
653
+ const score = 0;
537
654
 
538
- if (this._hintsIndex < this._hints.length) {
539
- const hintText = this._generateHintText(this._hints[this._hintsIndex]);
655
+ // Add score to total (only once per question)
656
+ if (!this._scoreAddedForCurrentQuestion) {
657
+ this.totalScore += score;
658
+ this._scoreAddedForCurrentQuestion = true;
659
+ }
540
660
 
541
- if (hintText) {
542
- this._hint = hintText;
543
- this._hintsIndex++;
544
- this._score0.subtract(HINT_COST);
661
+ // Fire-and-forget recallability feedback.
662
+ if (this._incident?.id) {
663
+ this._apiService.incidentRecallability(this._incident?.id, this._playerDob, is_correct);
545
664
  }
546
- }
547
665
 
548
- if (this._hintsIndex >= this._hints.length) {
549
- // this._hintsIndex = 0;
550
- this._showHint = false;
666
+ // Show toast notification
667
+ const message = this.i18n.translate("timeline-wrong")
668
+ .replace(/__YEAR__/, (this._correctYear || 0).toString())
669
+ .replace(/__YEAR1__/, this.from.year().toString())
670
+ .replace(/__YEAR2__/, this.to.year().toString());
671
+ this._showToast(message, 'wrong');
672
+ return;
551
673
  }
552
- }
553
674
 
554
- // renders the feedback overlay
555
- private _renderFeedback(): TemplateResult {
556
- // do not show anything until an answer has been made
557
- // and the score is greater than 0
558
- if (!this._answer && this._score0.value > 0) {
559
- return html``;
675
+ // do not process if no answer has been made
676
+ if (!this._clickedPosition) {
677
+ return;
560
678
  }
561
679
 
562
- // check if the answer is correct
563
- // cast to strings to compare
564
- const is_correct = this._answer + "" == this._solution + "";
565
- const score = is_correct ? this._score0.value : 0;
680
+ // check if the timeline position is correct
681
+ const is_correct = this._isTimelinePositionCorrect();
682
+ // Use remaining time as score (or 0 if wrong)
683
+ const score = is_correct ? Math.max(0, this._score0.value) : 0;
566
684
 
567
- if (this._score0.isRunning()) {
568
- this._score0.stop();
685
+ // Add score to total (only once per question)
686
+ if (!this._scoreAddedForCurrentQuestion) {
687
+ this.totalScore += score;
688
+ this._scoreAddedForCurrentQuestion = true;
569
689
  }
570
690
 
571
- this.totalScore += score;
691
+ // Add correct year to timeline only if answer is correct
692
+ if (is_correct && this._correctYear !== undefined) {
693
+ this._addYearToTimeline(this._correctYear);
694
+ }
572
695
 
573
696
  // Fire-and-forget recallability feedback.
574
697
  if (this._incident?.id) {
575
698
  this._apiService.incidentRecallability(this._incident?.id, this._playerDob, is_correct);
576
699
  }
577
700
 
701
+ // Find the bounds for the correct position
702
+ const sortedKnownYears = [...this._timelineYears].sort((a, b) => a - b);
703
+ let lowerBound: number = this.from.year();
704
+ let upperBound: number = this.to.year();
705
+
706
+ if (this._correctYear !== undefined) {
707
+ for (let i = 0; i < sortedKnownYears.length; i++) {
708
+ if (sortedKnownYears[i] < this._correctYear) {
709
+ lowerBound = sortedKnownYears[i];
710
+ } else if (sortedKnownYears[i] > this._correctYear && upperBound === this.to.year()) {
711
+ upperBound = sortedKnownYears[i];
712
+ break;
713
+ }
714
+ }
715
+ }
716
+
717
+ // Show toast message
578
718
  if (is_correct) {
579
- return html`
580
- <div class="feedback">
581
- <div class="feedback--content">
582
- <h3>${this.i18n.translate("answer-correct")}!</h3>
583
- <p>
584
- ${this._hintsIndex == 1 ? this.i18n.translate("used-hint-singular") : this.i18n.translate("used-hint-plural").replace(/__HINTS__/, this._hintsIndex + '')}
585
- </p>
586
- <p>
587
- ${score == 1 ? this.i18n.translate("number-points-singular") : this.i18n.translate("number-points-plural").replace(/__POINTS__/, score + '')}
588
- </p>
589
-
590
- ${this._renderNextQuestionButton()}
591
- </div>
592
- </div>
593
- `;
594
- } else if (!is_correct && this._answer) {
595
- return html`
596
- <div class="feedback">
597
- <div class="feedback--content">
598
- <h3>${this.i18n.translate("answer-wrong")}</h3>
599
- <p>
600
- ${this.i18n.translate("correct-answer-was").replace(/__WRONG__/, this._answer + '').replace(/__CORRECT__/, this._solution + '')}
601
- </p>
602
- ${this._renderNextQuestionButton()}
603
- </div>
604
- </div>
605
- `;
719
+ this._showToast(
720
+ this.i18n.translate("timeline-correct-with-year").replace(/__YEAR__/, this._correctYear + '').replace(/__YEAR1__/, lowerBound + '').replace(/__YEAR2__/, upperBound + ''),
721
+ 'correct'
722
+ );
606
723
  } else {
724
+ this._showToast(
725
+ this.i18n.translate("timeline-wrong").replace(/__YEAR__/, this._correctYear + '').replace(/__YEAR1__/, lowerBound + '').replace(/__YEAR2__/, upperBound + ''),
726
+ 'wrong'
727
+ );
728
+ }
729
+ }
730
+
731
+ // Renders the toast notification
732
+ private _renderToast(): TemplateResult {
733
+ if (this._toastMessage && this._toastType) {
607
734
  return html`
608
- <div class="feedback">
609
- <div class="feedback--content">
610
- <h3>Helaas</h3>
611
- <p>
612
- ${this.i18n.translate("no-answer-correct-answer-was").replace(/__CORRECT__/, this._solution + '')}
613
- </p>
614
- ${this._renderNextQuestionButton()}
735
+ <div class="toast toast-${this._toastType}">
736
+ <div class="toast-content">
737
+ <p>${this._toastMessage}</p>
615
738
  </div>
616
739
  </div>
617
740
  `;
618
741
  }
742
+ return html``;
619
743
  }
620
744
 
621
- // renders the next question button
622
- private _renderNextQuestionButton(): TemplateResult {
623
- if (this._numberOfQuestionsSeen < this.numberOfQuestions) {
624
- return html`
625
- <button @click="${this._loadQuestion}">${this.i18n.translate("next-question")}</button>
626
- `;
745
+ // Shows a toast notification that auto-dismisses after 3 seconds
746
+ private _showToast(message: string, type: 'correct' | 'wrong'): void {
747
+ // Clear any existing timeout
748
+ if (this._toastTimeout) {
749
+ clearTimeout(this._toastTimeout);
627
750
  }
628
751
 
629
- return html` <button @click="${this._endGame}">${this.i18n.translate("quit")}</button> `;
752
+ this._toastMessage = message;
753
+ this._toastType = type;
754
+ this.requestUpdate();
755
+
756
+ // Auto-dismiss after 3 seconds and load next question
757
+ this._toastTimeout = setTimeout(() => {
758
+ this._toastMessage = undefined;
759
+ this._toastType = undefined;
760
+ this.requestUpdate();
761
+
762
+ // Automatically load next question or end game
763
+ if (this._numberOfQuestionsSeen < this.numberOfQuestions) {
764
+ this._loadQuestion();
765
+ } else {
766
+ this._endGame();
767
+ }
768
+ }, 3000);
630
769
  }
631
770
 
771
+
632
772
  // renders the end of the game
633
773
  private _endGame(): void {
634
774
  this._renderState = GameState.END;
@@ -636,10 +776,10 @@ export class GuessTheYear extends LitElement {
636
776
 
637
777
  private renderGameEnd(): TemplateResult {
638
778
  console.debug("renderGameEnd");
639
- const maxScore: number = this.numberOfQuestions * this._score;
779
+ const maxScore: number = this.numberOfQuestions * this.timePerQuestion; // max points per question
640
780
  const scoreRatio = Math.round((this.totalScore / maxScore) * 100);
641
781
  return html`
642
- ${this._renderHeader("The end!")}
782
+ ${this._renderHeader(this.i18n.translate("the-end"))}
643
783
  <div class="guess-the-year-game-end">
644
784
  <p>
645
785
  ${this.i18n.translate("end").replace(/__SCORE__/, this.totalScore + '').replace(/__MAX_SCORE__/, maxScore + '').replace(/__PERCENT__/, scoreRatio + '')}
@@ -667,19 +807,41 @@ export class GuessTheYear extends LitElement {
667
807
  this.totalScore = 0;
668
808
  this._numberOfQuestionsSeen = 0;
669
809
 
810
+ // Initialize timeline with middle of the period
811
+ const middleYear = Math.floor((this.from.year() + this.to.year()) / 2);
812
+ this._timelineYears = [middleYear];
813
+
670
814
  this._loadQuestion();
671
815
  }
672
816
 
673
817
  private _resetStage(): void {
674
818
  this.sources = [];
675
819
  this._incident = undefined;
676
- this._hints = [];
677
- this._answer = undefined;
678
- this._answers = [];
679
- this._hintsIndex = 0;
820
+
821
+ // Reset user interaction flags
822
+ this._userClicked = false;
823
+
824
+ // Update score controller with current timePerQuestion value
825
+ this._score0.startValue = this.timePerQuestion;
680
826
  this._score0.reset();
681
- this._showHint = false;
682
- this._hint = undefined;
827
+ // Reset timeline interaction state, but keep known years
828
+ this._clickedPosition = undefined;
829
+ this._correctYear = undefined;
830
+ this._scoreAddedForCurrentQuestion = false;
831
+
832
+ // Clear toast
833
+ if (this._toastTimeout) {
834
+ clearTimeout(this._toastTimeout);
835
+ this._toastTimeout = undefined;
836
+ }
837
+ this._toastMessage = undefined;
838
+ this._toastType = undefined;
839
+ }
840
+
841
+ private _addYearToTimeline(year: number): void {
842
+ if (!this._timelineYears.includes(year)) {
843
+ this._timelineYears = [...this._timelineYears, year].sort((a, b) => a - b);
844
+ }
683
845
  }
684
846
 
685
847
  async _loadQuestion(): Promise<void> {
@@ -701,54 +863,13 @@ export class GuessTheYear extends LitElement {
701
863
  this._incident = response.incident;
702
864
  this.sources = response.sources;
703
865
 
704
- this._solution = Number(
866
+ this._solution = parseInt(
705
867
  (response.incident?.yearplusmonth || "").substring(0, 4),
706
- );
868
+ 10
869
+ ) || undefined;
707
870
 
708
- const answers = [this._solution];
709
- const from = this.from.year();
710
- const to = this.to.year();
711
-
712
- // generate 3 more random and unique answers between to and from
713
- while (answers.length < 4) {
714
- const randomAnswer = Math.floor(Math.random() * (to - from)) + from;
715
- if (!answers.includes(randomAnswer)) {
716
- answers.push(randomAnswer);
717
- }
718
- }
719
-
720
- // sort theses answers
721
- answers.sort();
722
-
723
- this._answers = answers;
724
-
725
- for (let i = 0; i < this.maxHints; i++) {
726
- console.debug("loading hint", i);
727
-
728
- const response: { incident: Incident | undefined; sources: string[] } =
729
- await this._apiService.fetchOneIncident(
730
- this.country,
731
- Util.shuffleArray([...Categories]).shift(),
732
- undefined,
733
- undefined,
734
- "",
735
- `${this._solution}-01-01`,
736
- `${this._solution}-12-31`,
737
- undefined,
738
- undefined,
739
- );
740
-
741
- if (response.incident) {
742
- this._hints.push(response.incident);
743
- }
744
- }
745
-
746
- if (this._hints.length === 0) {
747
- this._showHint = false;
748
- }
749
- else {
750
- this._showHint = true;
751
- }
871
+ // Store correct year for timeline
872
+ this._correctYear = this._solution;
752
873
 
753
874
  // start render game stage
754
875
  this._renderState = GameState.PLAY;
@@ -760,65 +881,6 @@ export class GuessTheYear extends LitElement {
760
881
  this._numberOfQuestionsSeen += 1;
761
882
  }
762
883
 
763
- private _generateHintText(hintIncident: Incident): string {
764
- let hint = "";
765
- const hintTemplates = this.i18n.HintTemplates[hintIncident.category];
766
- switch (hintIncident.category) {
767
- case Category.newsItem:
768
- hint = (
769
- hintTemplates[Math.round(Math.random() * hintTemplates.length)] || ""
770
- ).replace(/__TITLE__/, hintIncident.title);
771
- break;
772
- case Category.radioSong:
773
- // The title property of radioSongs always follow this pattern: track - artist
774
- const track = hintIncident.title.replace(/(.*?)\s+\-.*/, "$1");
775
- const artist = hintIncident.title.replace(/.*\-(.*)/, "$1");
776
- hint = (
777
- hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
778
- hintTemplates[0]
779
- )
780
- .replace(/__ARTIST__/, artist)
781
- .replace(/__TRACK__/, track);
782
- break;
783
- case Category.cinemaMovie:
784
- hint = (
785
- hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
786
- hintTemplates[0]
787
- ).replace(/__TITLE__/, hintIncident.title);
788
- break;
789
- case Category.tech:
790
- hint = (
791
- hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
792
- hintTemplates[0]
793
- ).replace(/__TITLE__/, hintIncident.title);
794
- break;
795
- case Category.newsPresenter:
796
- hint = (
797
- hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
798
- hintTemplates[0]
799
- ).replace(/__TITLE__/, hintIncident.title || hintIncident.text);
800
- break;
801
- case Category.tvShowOrSeries:
802
- hint = (
803
- hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
804
- hintTemplates[0]
805
- ).replace(/__TITLE__/, hintIncident.title || hintIncident.text);
806
- break;
807
- case Category.sports:
808
- hint = (
809
- hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
810
- hintTemplates[0]
811
- ).replace(/__TITLE__/, hintIncident.title || hintIncident.text);
812
- break;
813
- case Category.showbizz:
814
- hint = (
815
- hintTemplates[Math.round(Math.random() * hintTemplates.length)] ||
816
- hintTemplates[0]
817
- ).replace(/__TITLE__/, hintIncident.title || hintIncident.text);
818
- break;
819
- }
820
- return hint;
821
- }
822
884
 
823
885
  renderSources(): TemplateResult {
824
886
  console.debug("renderSources");