diff --git a/ui/recap/css/_recap.scss b/ui/recap/css/_recap.scss
index f6b4b5b7f07d3..52af541417de2 100644
--- a/ui/recap/css/_recap.scss
+++ b/ui/recap/css/_recap.scss
@@ -189,3 +189,21 @@ body {
transform: rotate(-90deg);
}
}
+
+.animated-pulse {
+ animation: pulse 1s 1;
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ }
+
+ 50% {
+ transform: scale(1.2);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+}
diff --git a/ui/recap/src/swiper.ts b/ui/recap/src/swiper.ts
index edd12701a1b87..c1c3cabfaec8c 100644
--- a/ui/recap/src/swiper.ts
+++ b/ui/recap/src/swiper.ts
@@ -4,8 +4,9 @@ import * as mod from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/bundle';
import { Recap } from './interfaces';
-import { animateNumber, formatDuration } from './ui';
+import { animateNumber } from './ui';
import { get } from 'common/data';
+import { formatDuration } from './util';
export const makeSwiper =
(_recap: Recap) =>
@@ -14,7 +15,7 @@ export const makeSwiper =
const progressContent = element.querySelector('.autoplay-progress span') as HTMLSpanElement;
const options: SwiperOptions = {
modules: [mod.Pagination, mod.Navigation, mod.Keyboard, mod.Mousewheel, mod.Autoplay],
- initialSlide: 3,
+ initialSlide: window.location.hash ? parseInt(window.location.hash.slice(1)) : 0,
direction: 'horizontal',
loop: false,
cssMode: false,
@@ -50,6 +51,14 @@ export const makeSwiper =
.forEach((counter: HTMLElement) => {
animateNumber(counter, { duration: 1000, render: formatDuration });
});
+ element
+ .querySelectorAll('.swiper-slide-active .animated-pulse')
+ .forEach((counter: HTMLElement) => {
+ counter.classList.remove('animated-pulse');
+ setTimeout(() => {
+ counter.classList.add('animated-pulse');
+ }, 100);
+ });
element.querySelectorAll('.swiper-slide-active .lpv').forEach((el: HTMLElement) => {
const lpv = get(el, 'lpv')!;
lpv.goTo('first');
diff --git a/ui/recap/src/ui.ts b/ui/recap/src/ui.ts
index 098007769e64e..c34a2d960c3c2 100644
--- a/ui/recap/src/ui.ts
+++ b/ui/recap/src/ui.ts
@@ -48,22 +48,3 @@ export const loadOpeningLpv = (el: HTMLElement, color: Color, opening: Opening):
});
set(lpv.div!, 'lpv', lpv);
};
-
-export function formatDuration(seconds: number): string {
- const d = Math.floor(seconds / (24 * 3600));
- const h = Math.floor((seconds % (24 * 3600)) / 3600);
- const m = Math.floor((seconds % 3600) / 60);
-
- let result: string[] = [];
- if (d > 0) {
- result.push(simplePlural(d, 'day'));
- }
- result.push(simplePlural(h, 'hour'));
- result.push(simplePlural(m, 'minute'));
-
- return result.slice(0, 2).join('
');
-}
-
-function simplePlural(n: number, unit: string): string {
- return `${n} ${unit}${n === 1 ? '' : 's'}`;
-}
diff --git a/ui/recap/src/util.ts b/ui/recap/src/util.ts
new file mode 100644
index 0000000000000..19c4abd0415d8
--- /dev/null
+++ b/ui/recap/src/util.ts
@@ -0,0 +1,18 @@
+export function formatDuration(seconds: number): string {
+ const d = Math.floor(seconds / (24 * 3600));
+ const h = Math.floor((seconds % (24 * 3600)) / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+
+ let result: string[] = [];
+ if (d > 0) {
+ result.push(simplePlural(d, 'day'));
+ }
+ result.push(simplePlural(h, 'hour'));
+ result.push(simplePlural(m, 'minute'));
+
+ return result.slice(0, 2).join('
');
+}
+
+function simplePlural(n: number, unit: string): string {
+ return `${n} ${unit}${n === 1 ? '' : 's'}`;
+}
diff --git a/ui/recap/src/view.ts b/ui/recap/src/view.ts
index 972cf223ba036..fd278fa69bdf4 100644
--- a/ui/recap/src/view.ts
+++ b/ui/recap/src/view.ts
@@ -77,7 +77,7 @@ const nbMoves = (r: Recap): VNode => {
const firstMoves = (r: Recap, firstMove: Counted): VNode => {
const percent = Math.round((firstMove.count * 100) / r.games.nbWhite);
return slideTag('first')([
- h('div.recap--massive', [h('strong', '1. ' + firstMove.value)]),
+ h('div.recap--massive', [h('strong.animated-pulse', '1. ' + firstMove.value)]),
h('div', [
h('p', [
'is how you started ',
@@ -120,7 +120,7 @@ const openingColor = (os: ByColor>, color: Color): VNode => {
const malware = () =>
slideTag('malware')([
- h('div.recap--massive', [h('strong', '0'), 'ads and trackers loaded']),
+ h('div.recap--massive', [h('strong.animated-pulse', '0'), 'ads and trackers loaded']),
h('p', "We didn't sell your personal data, and we didn't use your device against you."),
h(
'p',
diff --git a/ui/recap/tests/duration.test.ts b/ui/recap/tests/duration.test.ts
index c1104f1e1f1af..3f09b8f7fec85 100644
--- a/ui/recap/tests/duration.test.ts
+++ b/ui/recap/tests/duration.test.ts
@@ -1,25 +1,25 @@
import { expect, test } from 'vitest';
-import { showDuration } from '../src/util';
+import { formatDuration } from '../src/util';
test('singular and plural time units', () => {
- expect(showDuration(0)).toBe('0 minutes');
- expect(showDuration(1)).toBe('0 minutes');
- expect(showDuration(2)).toBe('0 minutes');
+ expect(formatDuration(0)).toBe('0 hours
0 minutes');
+ expect(formatDuration(1)).toBe('0 hours
0 minutes');
+ expect(formatDuration(2)).toBe('0 hours
0 minutes');
- expect(showDuration(60)).toBe('1 minute');
- expect(showDuration(120)).toBe('2 minutes');
+ expect(formatDuration(60)).toBe('0 hours
1 minute');
+ expect(formatDuration(120)).toBe('0 hours
2 minutes');
- expect(showDuration(60 * 60 * 1)).toBe('1 hour');
- expect(showDuration(60 * 60 * 2)).toBe('2 hours');
+ expect(formatDuration(60 * 60 * 1)).toBe('1 hour
0 minutes');
+ expect(formatDuration(60 * 60 * 2)).toBe('2 hours
0 minutes');
- expect(showDuration(60 * 60 * 1 + 60 * 1)).toBe('1 hour and 1 minute');
- expect(showDuration(60 * 60 * 1 + 60 * 2)).toBe('1 hour and 2 minutes');
- expect(showDuration(60 * 60 * 2 + 60 * 1)).toBe('2 hours and 1 minute');
- expect(showDuration(60 * 60 * 2 + 60 * 2)).toBe('2 hours and 2 minutes');
+ expect(formatDuration(60 * 60 * 1 + 60 * 1)).toBe('1 hour
1 minute');
+ expect(formatDuration(60 * 60 * 1 + 60 * 2)).toBe('1 hour
2 minutes');
+ expect(formatDuration(60 * 60 * 2 + 60 * 1)).toBe('2 hours
1 minute');
+ expect(formatDuration(60 * 60 * 2 + 60 * 2)).toBe('2 hours
2 minutes');
- expect(showDuration(60 * 60 * 24 * 1)).toBe('1 day');
- expect(showDuration(60 * 60 * 24 * 2)).toBe('2 days');
+ expect(formatDuration(60 * 60 * 24 * 1)).toBe('1 day
0 hours');
+ expect(formatDuration(60 * 60 * 24 * 2)).toBe('2 days
0 hours');
- expect(showDuration(60 * 60 * 24 * 1 + 60 * 60 * 1 + 60 * 1)).toBe('1 day and 1 hour');
- expect(showDuration(60 * 60 * 24 * 2 + 60 * 60 * 2 + 60 * 2)).toBe('2 days and 2 hours');
+ expect(formatDuration(60 * 60 * 24 * 1 + 60 * 60 * 1 + 60 * 1)).toBe('1 day
1 hour');
+ expect(formatDuration(60 * 60 * 24 * 2 + 60 * 60 * 2 + 60 * 2)).toBe('2 days
2 hours');
});