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'); });