diff --git a/apps/site/components/Common/Banner/index.module.css b/apps/site/components/Common/Banner/index.module.css index f3c2b9c1ec77c..57e3579d1ae02 100644 --- a/apps/site/components/Common/Banner/index.module.css +++ b/apps/site/components/Common/Banner/index.module.css @@ -1,13 +1,19 @@ .banner { @apply flex - w-full flex-row - items-center - justify-center - gap-2 - px-8 - py-3 - text-sm; + items-center; + + .content { + @apply flex + w-full + flex-row + items-center + justify-center + gap-2 + px-8 + py-3 + text-sm; + } &, a { @@ -25,6 +31,19 @@ @apply size-4 text-white/50; } + + .close { + @apply pr-4 + transition-transform; + + &:hover { + @apply text-white; + + svg { + @apply scale-110; + } + } + } } .default { diff --git a/apps/site/components/Common/Banner/index.stories.tsx b/apps/site/components/Common/Banner/index.stories.tsx index cf489c8d983a6..0c034d02e8115 100644 --- a/apps/site/components/Common/Banner/index.stories.tsx +++ b/apps/site/components/Common/Banner/index.stories.tsx @@ -36,4 +36,13 @@ export const NoLink: Story = { }, }; +export const Hideable: Story = { + args: { + children: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + type: 'default', + link: '/', + onHiding: () => {}, + }, +}; + export default { component: Banner } as Meta; diff --git a/apps/site/components/Common/Banner/index.tsx b/apps/site/components/Common/Banner/index.tsx index f72b98e15a79e..576fdeba0ba95 100644 --- a/apps/site/components/Common/Banner/index.tsx +++ b/apps/site/components/Common/Banner/index.tsx @@ -1,4 +1,6 @@ -import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; +import { ArrowUpRightIcon, XMarkIcon } from '@heroicons/react/24/outline'; +import classNames from 'classnames'; +import { useTranslations } from 'next-intl'; import type { FC, PropsWithChildren } from 'react'; import Link from '@/components/Link'; @@ -8,17 +10,35 @@ import styles from './index.module.css'; type BannerProps = { link?: string; type?: 'default' | 'warning' | 'error'; + onHiding?: () => void; }; const Banner: FC> = ({ type = 'default', link, + onHiding, children, -}) => ( -
- {link ? {children} : children} - {link && } -
-); +}) => { + const t = useTranslations('components.common.banner'); + + return ( +
+ + {link ? {children} : children} + {link && } + + {onHiding && ( + + )} +
+ ); +}; export default Banner; diff --git a/apps/site/components/withBanner.tsx b/apps/site/components/withBanner.tsx index af571479ad8f4..2ee3a4d31b3a0 100644 --- a/apps/site/components/withBanner.tsx +++ b/apps/site/components/withBanner.tsx @@ -1,21 +1,53 @@ +'use client'; + +import { useEffect, useState } from 'react'; import type { FC } from 'react'; import Banner from '@/components/Common/Banner'; +import { useLocalStorage } from '@/hooks'; import { siteConfig } from '@/next.json.mjs'; import { dateIsBetween } from '@/util/dateUtils'; +import { twoDateToUIID } from '@/util/stringUtils'; + +type BannerState = { + uuid: string; + hideBanner: boolean; +}; const WithBanner: FC<{ section: string }> = ({ section }) => { const banner = siteConfig.websiteBanners[section]; + const UUID = twoDateToUIID(banner.startDate, banner.endDate); + const [shouldDisplay, setShouldDisplay] = useState(false); + const [bannerState, setBannerState] = useLocalStorage('banner', { + uuid: UUID, + hideBanner: false, + }); - if (banner && dateIsBetween(banner.startDate, banner.endDate)) { + useEffect(() => { + if (dateIsBetween(banner.startDate, banner.endDate)) { + setShouldDisplay(!bannerState.hideBanner); + } + if (bannerState.uuid !== UUID) { + setBannerState({ uuid: UUID, hideBanner: false }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [bannerState.hideBanner]); + + const handleBannerHiding = () => { + setBannerState({ ...bannerState, hideBanner: true }); + }; + + if (shouldDisplay) { return ( - + {banner.text} ); } - - return null; }; export default WithBanner; diff --git a/apps/site/hooks/react-client/__tests__/useLocaleStorage.test.mjs b/apps/site/hooks/react-client/__tests__/useLocaleStorage.test.mjs new file mode 100644 index 0000000000000..0eb44a9edd229 --- /dev/null +++ b/apps/site/hooks/react-client/__tests__/useLocaleStorage.test.mjs @@ -0,0 +1,45 @@ +import { render } from '@testing-library/react'; +import { act } from 'react-dom/test-utils'; + +import { useLocalStorage } from '@/hooks/react-client/useLocalStorage'; + +describe('useLocalStorage', () => { + beforeEach(() => { + Object.defineProperty(window, 'localStorage', { + value: { + getItem: jest.fn(), + setItem: jest.fn(), + }, + writable: true, + }); + }); + + it('should initialize with the provided initial value', () => { + render(() => { + const [value] = useLocalStorage('testKey', 'initialValue'); + expect(value).toBe('initialValue'); + }); + }); + + it('should update localStorage when value changes', () => { + render(() => { + const TestComponent = () => { + const [value, setValue] = useLocalStorage('testKey', 'initialValue'); + + act(() => { + setValue('newValue'); + }); + + return
{value}
; + }; + + const { container } = render(); + + expect(window.localStorage.setItem).toHaveBeenCalledWith( + 'testKey', + JSON.stringify('newValue') + ); + expect(container.textContent).toBe('newValue'); + }); + }); +}); diff --git a/apps/site/hooks/react-client/index.ts b/apps/site/hooks/react-client/index.ts index c78f8d3cfbc38..52912ba766ba3 100644 --- a/apps/site/hooks/react-client/index.ts +++ b/apps/site/hooks/react-client/index.ts @@ -7,3 +7,4 @@ export { default as useKeyboardCommands } from './useKeyboardCommands'; export { default as useClickOutside } from './useClickOutside'; export { default as useBottomScrollListener } from './useBottomScrollListener'; export { default as useNavigationState } from './useNavigationState'; +export { default as useLocalStorage } from './useLocalStorage'; diff --git a/apps/site/hooks/react-client/useLocalStorage.ts b/apps/site/hooks/react-client/useLocalStorage.ts new file mode 100644 index 0000000000000..e850f51d512eb --- /dev/null +++ b/apps/site/hooks/react-client/useLocalStorage.ts @@ -0,0 +1,22 @@ +'use client'; +import { useEffect, useState } from 'react'; + +const useLocalStorage = (key: string, initialValue?: T) => { + const [value, setValue] = useState(() => { + if (typeof window !== 'undefined') { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } + return initialValue; + }); + + useEffect(() => { + if (typeof window !== 'undefined') { + window.localStorage.setItem(key, JSON.stringify(value)); + } + }, [key, value]); + + return [value, setValue] as const; +}; + +export default useLocalStorage; diff --git a/apps/site/util/__tests__/stringUtils.test.mjs b/apps/site/util/__tests__/stringUtils.test.mjs index 27e65f1b0355e..0be8f7d59baec 100644 --- a/apps/site/util/__tests__/stringUtils.test.mjs +++ b/apps/site/util/__tests__/stringUtils.test.mjs @@ -2,6 +2,7 @@ import { getAcronymFromString, parseRichTextIntoPlainText, dashToCamelCase, + twoDateToUIID, } from '@/util/stringUtils'; describe('String utils', () => { @@ -72,4 +73,10 @@ describe('String utils', () => { it('dashToCamelCase returns correct camelCase with numbers', () => { expect(dashToCamelCase('foo-123-bar')).toBe('foo123Bar'); }); + + it('twoDateToUIID returns the correct UUID', () => { + const date = '2024-01-01T00:00:00.000Z'; + const date2 = '2024-01-01T00:00:00.000Z'; + expect(twoDateToUIID(date, date2)).toBe('2024-01-01-2024-01-01'); + }); }); diff --git a/apps/site/util/stringUtils.ts b/apps/site/util/stringUtils.ts index d830a6dfc4f2c..3861c8d66e418 100644 --- a/apps/site/util/stringUtils.ts +++ b/apps/site/util/stringUtils.ts @@ -28,3 +28,10 @@ export const dashToCamelCase = (str: string) => // remove leftover - which don't match the above regex. Like 'es-2015' .replace(/-/g, '') .replace(/^[A-Z]/, chr => chr.toLowerCase()); + +export const twoDateToUIID = (date1: string, date2: string): string => { + const date1String = date1.split('T')[0]; + const date2String = date2.split('T')[0]; + + return `${date1String}-${date2String}`; +}; diff --git a/packages/i18n/locales/en.json b/packages/i18n/locales/en.json index 096896b6f6057..f17c3df22514a 100644 --- a/packages/i18n/locales/en.json +++ b/packages/i18n/locales/en.json @@ -159,6 +159,9 @@ "previous": "Previous" }, "common": { + "banner": { + "hide": "Dismiss this banner" + }, "breadcrumbs": { "navigateToHome": "Navigate to Home" },