From b943866107691c22bddd46a1745441e826b551c4 Mon Sep 17 00:00:00 2001 From: Bryan Chu Date: Fri, 10 Jan 2025 10:14:59 -0800 Subject: [PATCH 01/10] --- .../MLChallenge/MLChallengeBanner.tsx | 62 +++++-------------- .../data-portal/app/constants/localStorage.ts | 1 + .../public/locales/en/translation.json | 3 +- 3 files changed, 18 insertions(+), 48 deletions(-) diff --git a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.tsx b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.tsx index 3dda725ac..e78c9d215 100644 --- a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.tsx +++ b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.tsx @@ -1,75 +1,45 @@ import { Banner } from '@czi-sds/components' -import { useLocalStorageValue } from '@react-hookz/web' import { useLocation } from '@remix-run/react' -import dayjs from 'dayjs' import { useState } from 'react' -import { match, P } from 'ts-pattern' -import { I18n } from 'app/components/I18n' import { LocalStorageKeys } from 'app/constants/localStorage' import { useEffectOnce } from 'app/hooks/useEffectOnce' -import { I18nKeys } from 'app/types/i18n' +import { useI18n } from 'app/hooks/useI18n' -const BANNER_ALLOWLIST = [/^\/$/, /^\/browse-data\/.*$/] -const ML_CHALLENGE_END_DATE = dayjs('February 6, 2025') - -// TODO(jeremy) check with team to see what the correct interval is -const ML_CHALLENGE_END_INTERVAL = 10 - -const ML_CHALLENGE_END_NOTIFY_DATE = ML_CHALLENGE_END_DATE.subtract( - ML_CHALLENGE_END_INTERVAL, - 'days', -) +const BANNER_PATHS = [/^\/$/, /^\/browse-data\/.*$/] export function MLChallengeBanner() { - const { - value: lastDismissedBannerMessage, - set: setLastDismissedBannerMessage, - } = useLocalStorageValue( - LocalStorageKeys.CompetitionBannerDismissed, - { defaultValue: null }, - ) + const { t } = useI18n() const [open, setOpen] = useState(false) const location = useLocation() - const now = dayjs() - - const bannerI18nKey = match(now) - .with( - P.when((d) => d.isAfter(ML_CHALLENGE_END_DATE)), - () => 'mlCompetitionEnded' as I18nKeys, - ) - .with( - P.when((d) => d.isAfter(ML_CHALLENGE_END_NOTIFY_DATE)), - () => 'mlCompetitionEnding' as I18nKeys, - ) - .otherwise(() => 'mlCompetitionHasBegun' as I18nKeys) - // open banner on client side to prevent flash of content since local storage // is not available when server-side rendering. - useEffectOnce(() => setOpen(bannerI18nKey !== lastDismissedBannerMessage)) + useEffectOnce(() => + setOpen( + localStorage.getItem( + LocalStorageKeys.CompetitionEndingBannerDismissed, + ) !== 'true', + ), + ) return ( !regex.test(location.pathname)) + !open || BANNER_PATHS.every((regex) => !regex.test(location.pathname)) } dismissible sdsType="primary" onClose={() => { setOpen(false) - setLastDismissedBannerMessage(bannerI18nKey) + localStorage.setItem( + LocalStorageKeys.CompetitionEndingBannerDismissed, + 'true', + ) }} >
- + {t('mlChallengeIsClosingSoon')}
) diff --git a/frontend/packages/data-portal/app/constants/localStorage.ts b/frontend/packages/data-portal/app/constants/localStorage.ts index 70d9ec8a3..975851517 100644 --- a/frontend/packages/data-portal/app/constants/localStorage.ts +++ b/frontend/packages/data-portal/app/constants/localStorage.ts @@ -1,5 +1,6 @@ export enum LocalStorageKeys { CompetitionBannerDismissed = 'competition-banner-dismissed', + CompetitionEndingBannerDismissed = 'competition-ending-banner-dismissed', SurveyBannerDismissed = 'survey-banner-dismissed', TableRenderErrorPageReloadCount = 'table-render-error-page-reload-count', } diff --git a/frontend/packages/data-portal/public/locales/en/translation.json b/frontend/packages/data-portal/public/locales/en/translation.json index 5614b28d0..df667b9a2 100644 --- a/frontend/packages/data-portal/public/locales/en/translation.json +++ b/frontend/packages/data-portal/public/locales/en/translation.json @@ -241,8 +241,7 @@ "microscopeModel": "Microscope model", "mlChallengeEndDate": "February 5, 2025", "mlChallengeStartDate": "November 6, 2024", - "mlCompetitionEnded": "Congratulations to the ML Competition winners! $t(mlCompetitionLearnMore) about the winning models.", - "mlCompetitionEnding": "ML Competition is ending in {{days}} days. Compete for cash prizes. $t(mlCompetitionLearnMore)", + "mlChallengeIsClosingSoon": "ML Competition is closing soon. Enter by 1/29 for cash prizes. $t(mlCompetitionLearnMore)", "mlCompetitionHasBegun": "ML Competition has begun. Enter for cash prizes. $t(mlCompetitionLearnMore)", "mlCompetitionLearnMore": "Learn More", "modelWeights": "Model Weights", From d092b3e0df3d9b17b080cbf575117e3ba13f20c0 Mon Sep 17 00:00:00 2001 From: Bryan Chu Date: Fri, 10 Jan 2025 10:27:48 -0800 Subject: [PATCH 02/10] --- .../app/components/MLChallenge/MLChallengeBanner.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.tsx b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.tsx index e78c9d215..9826453d4 100644 --- a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.tsx +++ b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.tsx @@ -4,14 +4,14 @@ import { useState } from 'react' import { LocalStorageKeys } from 'app/constants/localStorage' import { useEffectOnce } from 'app/hooks/useEffectOnce' -import { useI18n } from 'app/hooks/useI18n' + +import { I18n } from '../I18n' const BANNER_PATHS = [/^\/$/, /^\/browse-data\/.*$/] export function MLChallengeBanner() { - const { t } = useI18n() - const [open, setOpen] = useState(false) const location = useLocation() + const [open, setOpen] = useState(false) // open banner on client side to prevent flash of content since local storage // is not available when server-side rendering. @@ -39,7 +39,7 @@ export function MLChallengeBanner() { }} >
- {t('mlChallengeIsClosingSoon')} +
) From 5be3f5e01690fedb0cc430520268a776588b5b9a Mon Sep 17 00:00:00 2001 From: Bryan Chu Date: Fri, 10 Jan 2025 10:40:20 -0800 Subject: [PATCH 03/10] --- .../MLChallenge/MLChallengeBanner.test.tsx | 41 +++---------------- .../MLChallenge/MLChallengeBanner.tsx | 2 +- .../public/locales/en/translation.json | 2 +- 3 files changed, 8 insertions(+), 37 deletions(-) diff --git a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx index da82df497..1653ebebb 100644 --- a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx +++ b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx @@ -2,9 +2,8 @@ import { beforeEach, jest } from '@jest/globals' import { render, screen } from '@testing-library/react' import { MockI18n } from 'app/components/I18n.mock' -import { LocalStorageMock } from 'app/mocks/LocalStorage.mock' import { RemixMock } from 'app/mocks/Remix.mock' -import { getMockUser, setMockTime } from 'app/utils/mock' +import { getMockUser } from 'app/utils/mock' async function renderMlChallengeBanner() { const { MLChallengeBanner } = await import('./MLChallengeBanner') @@ -14,12 +13,9 @@ async function renderMlChallengeBanner() { jest.unstable_mockModule('app/components/I18n', () => ({ I18n: MockI18n })) const remixMock = new RemixMock() -const localStorageMock = new LocalStorageMock() describe('', () => { beforeEach(() => { - jest.useRealTimers() - localStorageMock.reset() remixMock.reset() }) @@ -39,52 +35,27 @@ describe('', () => { expect(screen.queryByRole('banner')).not.toBeInTheDocument() }) - it('should render challenge began message', async () => { - setMockTime('2024-12-01') - - await renderMlChallengeBanner() - expect(screen.getByText('mlCompetitionHasBegun')).toBeVisible() - }) - it('should render challenge ending message', async () => { - setMockTime('2025-01-30') - await renderMlChallengeBanner() - expect(screen.getByText('mlCompetitionEnding')).toBeVisible() - }) - - it('should render challenge ended message', async () => { - setMockTime('2025-02-07') - - await renderMlChallengeBanner() - expect(screen.getByText('mlCompetitionEnded')).toBeVisible() + expect(screen.getByText('mlCompetitionIsClosingSoon')).toBeVisible() }) it('should not render banner if was dismissed', async () => { - setMockTime('2024-12-01') - localStorageMock.mockValue('mlCompetitionHasBegun') - + localStorage.set('competition-ending-banner-dismissed', 'true') await renderMlChallengeBanner() expect(screen.queryByRole('banner')).not.toBeInTheDocument() }) - it('should render banner if last dismissed was previous state', async () => { - setMockTime('2025-01-30') - localStorageMock.mockValue('mlCompetitionHasBegun') - + it('should still render banner if dismissed was previous banner', async () => { + localStorage.set('competition-banner-dismissed', 'true') await renderMlChallengeBanner() expect(screen.getByRole('banner')).toBeVisible() }) it('should dismiss banner on click', async () => { - setMockTime('2024-12-01') - await renderMlChallengeBanner() await getMockUser().click(screen.getByRole('button')) - expect(screen.queryByRole('banner')).not.toBeInTheDocument() - expect(localStorageMock.setValue).toHaveBeenCalledWith( - 'mlCompetitionHasBegun', - ) + expect(localStorage.setItem).toHaveBeenCalledWith('true') }) }) diff --git a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.tsx b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.tsx index 9826453d4..cc545464e 100644 --- a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.tsx +++ b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.tsx @@ -39,7 +39,7 @@ export function MLChallengeBanner() { }} >
- +
) diff --git a/frontend/packages/data-portal/public/locales/en/translation.json b/frontend/packages/data-portal/public/locales/en/translation.json index df667b9a2..710065fe5 100644 --- a/frontend/packages/data-portal/public/locales/en/translation.json +++ b/frontend/packages/data-portal/public/locales/en/translation.json @@ -241,7 +241,7 @@ "microscopeModel": "Microscope model", "mlChallengeEndDate": "February 5, 2025", "mlChallengeStartDate": "November 6, 2024", - "mlChallengeIsClosingSoon": "ML Competition is closing soon. Enter by 1/29 for cash prizes. $t(mlCompetitionLearnMore)", + "mlCompetitionIsClosingSoon": "ML Competition is closing soon. Enter by 1/29 for cash prizes. $t(mlCompetitionLearnMore)", "mlCompetitionHasBegun": "ML Competition has begun. Enter for cash prizes. $t(mlCompetitionLearnMore)", "mlCompetitionLearnMore": "Learn More", "modelWeights": "Model Weights", From 35f4429d480b05a6d2dafb69e91a048ab5ce39d9 Mon Sep 17 00:00:00 2001 From: Bryan Chu Date: Fri, 10 Jan 2025 10:58:10 -0800 Subject: [PATCH 04/10] --- .../components/MLChallenge/MLChallengeBanner.test.tsx | 11 ++++------- .../data-portal/app/constants/localStorage.ts | 3 ++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx index 1653ebebb..d410922ed 100644 --- a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx +++ b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx @@ -17,6 +17,8 @@ const remixMock = new RemixMock() describe('', () => { beforeEach(() => { remixMock.reset() + jest.spyOn(Storage.prototype, 'getItem') + jest.spyOn(Storage.prototype, 'setItem') }) const paths = ['/', '/browse-data/datasets', '/browse-data/depositions'] @@ -41,18 +43,13 @@ describe('', () => { }) it('should not render banner if was dismissed', async () => { - localStorage.set('competition-ending-banner-dismissed', 'true') + jest.spyOn(Storage.prototype, 'getItem').mockReturnValue('true') await renderMlChallengeBanner() expect(screen.queryByRole('banner')).not.toBeInTheDocument() }) - it('should still render banner if dismissed was previous banner', async () => { - localStorage.set('competition-banner-dismissed', 'true') - await renderMlChallengeBanner() - expect(screen.getByRole('banner')).toBeVisible() - }) - it('should dismiss banner on click', async () => { + jest.spyOn(Storage.prototype, 'setItem') await renderMlChallengeBanner() await getMockUser().click(screen.getByRole('button')) expect(screen.queryByRole('banner')).not.toBeInTheDocument() diff --git a/frontend/packages/data-portal/app/constants/localStorage.ts b/frontend/packages/data-portal/app/constants/localStorage.ts index 975851517..435f5e343 100644 --- a/frontend/packages/data-portal/app/constants/localStorage.ts +++ b/frontend/packages/data-portal/app/constants/localStorage.ts @@ -1,6 +1,7 @@ export enum LocalStorageKeys { - CompetitionBannerDismissed = 'competition-banner-dismissed', CompetitionEndingBannerDismissed = 'competition-ending-banner-dismissed', SurveyBannerDismissed = 'survey-banner-dismissed', TableRenderErrorPageReloadCount = 'table-render-error-page-reload-count', + // DEPRECATED - DO NOT USE (keep these in the enum so we know not to use them in the future): + CompetitionBannerDismissed = 'competition-banner-dismissed', } From 36869e171bfb5ab484808ee527be58e726201792 Mon Sep 17 00:00:00 2001 From: Bryan Chu Date: Fri, 10 Jan 2025 11:20:47 -0800 Subject: [PATCH 05/10] test --- .../app/components/MLChallenge/MLChallengeBanner.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx index d410922ed..e3b0eb44b 100644 --- a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx +++ b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx @@ -51,6 +51,7 @@ describe('', () => { it('should dismiss banner on click', async () => { jest.spyOn(Storage.prototype, 'setItem') await renderMlChallengeBanner() + expect(screen.queryByRole('banner')).toBeVisible() await getMockUser().click(screen.getByRole('button')) expect(screen.queryByRole('banner')).not.toBeInTheDocument() expect(localStorage.setItem).toHaveBeenCalledWith('true') From e5b05c52c70911cb61a27873f138a7d3ef7f3ade Mon Sep 17 00:00:00 2001 From: Bryan Chu Date: Fri, 10 Jan 2025 11:27:59 -0800 Subject: [PATCH 06/10] clear mocks --- .../app/components/MLChallenge/MLChallengeBanner.test.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx index e3b0eb44b..68eb9840a 100644 --- a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx +++ b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx @@ -20,6 +20,9 @@ describe('', () => { jest.spyOn(Storage.prototype, 'getItem') jest.spyOn(Storage.prototype, 'setItem') }) + afterEach(() => { + jest.clearAllMocks() + }) const paths = ['/', '/browse-data/datasets', '/browse-data/depositions'] From 569cec240ffd5cac09d1a8714d79270a97df3b4a Mon Sep 17 00:00:00 2001 From: Bryan Chu Date: Fri, 10 Jan 2025 11:39:12 -0800 Subject: [PATCH 07/10] try again --- .../app/components/MLChallenge/MLChallengeBanner.test.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx index 68eb9840a..2e8f33932 100644 --- a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx +++ b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx @@ -16,12 +16,8 @@ const remixMock = new RemixMock() describe('', () => { beforeEach(() => { - remixMock.reset() - jest.spyOn(Storage.prototype, 'getItem') - jest.spyOn(Storage.prototype, 'setItem') - }) - afterEach(() => { jest.clearAllMocks() + remixMock.reset() }) const paths = ['/', '/browse-data/datasets', '/browse-data/depositions'] From 7421d1f2c79f57f788b7acf6ab5b48a020843da4 Mon Sep 17 00:00:00 2001 From: Bryan Chu Date: Fri, 10 Jan 2025 11:49:27 -0800 Subject: [PATCH 08/10] home --- .../MLChallenge/MLChallengeBanner.test.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx index 2e8f33932..b4466b689 100644 --- a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx +++ b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx @@ -25,33 +25,47 @@ describe('', () => { paths.forEach((pathname) => { it(`should render on ${pathname}`, async () => { remixMock.mockPathname(pathname) + await renderMlChallengeBanner() + expect(screen.queryByRole('banner')).toBeVisible() }) }) it('should not render on blocked pages', async () => { remixMock.mockPathname('/competition') + await renderMlChallengeBanner() + expect(screen.queryByRole('banner')).not.toBeInTheDocument() }) it('should render challenge ending message', async () => { + remixMock.mockPathname('/') + await renderMlChallengeBanner() + expect(screen.getByText('mlCompetitionIsClosingSoon')).toBeVisible() }) it('should not render banner if was dismissed', async () => { + remixMock.mockPathname('/') jest.spyOn(Storage.prototype, 'getItem').mockReturnValue('true') + await renderMlChallengeBanner() + expect(screen.queryByRole('banner')).not.toBeInTheDocument() }) it('should dismiss banner on click', async () => { jest.spyOn(Storage.prototype, 'setItem') + remixMock.mockPathname('/') await renderMlChallengeBanner() + expect(screen.queryByRole('banner')).toBeVisible() + await getMockUser().click(screen.getByRole('button')) + expect(screen.queryByRole('banner')).not.toBeInTheDocument() expect(localStorage.setItem).toHaveBeenCalledWith('true') }) From a523be21c9e0ad8b069c43232839b36f2dd45ca1 Mon Sep 17 00:00:00 2001 From: Bryan Chu Date: Fri, 10 Jan 2025 11:57:40 -0800 Subject: [PATCH 09/10] relaly --- .../app/components/MLChallenge/MLChallengeBanner.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx index b4466b689..cfd4b4a6e 100644 --- a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx +++ b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx @@ -58,6 +58,7 @@ describe('', () => { }) it('should dismiss banner on click', async () => { + jest.spyOn(Storage.prototype, 'getItem').mockReturnValue('false') jest.spyOn(Storage.prototype, 'setItem') remixMock.mockPathname('/') await renderMlChallengeBanner() From 0ad4c1d9d8f0430c0930a892b0cc6f3f03e5b1f2 Mon Sep 17 00:00:00 2001 From: Bryan Chu Date: Fri, 10 Jan 2025 12:01:59 -0800 Subject: [PATCH 10/10] --- .../app/components/MLChallenge/MLChallengeBanner.test.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx index cfd4b4a6e..4dd8e1cc0 100644 --- a/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx +++ b/frontend/packages/data-portal/app/components/MLChallenge/MLChallengeBanner.test.tsx @@ -58,7 +58,7 @@ describe('', () => { }) it('should dismiss banner on click', async () => { - jest.spyOn(Storage.prototype, 'getItem').mockReturnValue('false') + jest.spyOn(Storage.prototype, 'getItem').mockReturnValue(null) jest.spyOn(Storage.prototype, 'setItem') remixMock.mockPathname('/') await renderMlChallengeBanner() @@ -68,6 +68,9 @@ describe('', () => { await getMockUser().click(screen.getByRole('button')) expect(screen.queryByRole('banner')).not.toBeInTheDocument() - expect(localStorage.setItem).toHaveBeenCalledWith('true') + expect(localStorage.setItem).toHaveBeenCalledWith( + 'competition-ending-banner-dismissed', + 'true', + ) }) })