diff options
30 files changed, 308 insertions, 261 deletions
diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 825a5f8..1b7a07e 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -623,5 +623,11 @@ module.exports = { 'react/jsx-no-literals': 'off', }, }, + { + files: ['tests/**'], + rules: { + 'react/jsx-no-literals': 'off', + }, + }, ], }; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 2b7a466..de6afd3 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,12 +1,13 @@ import type { Decorator, Preview } from '@storybook/react'; import { ThemeProvider, useTheme } from 'next-themes'; +import { useDarkMode } from 'storybook-dark-mode'; import { FC, ReactNode, useEffect } from 'react'; import { IntlProvider } from 'react-intl'; -import { useDarkMode } from 'storybook-dark-mode'; +import { AckeeProvider } from '../src/utils/providers'; +import '../src/styles/globals.scss'; import { DocsContainer } from './overrides/docs-container'; import dark from './themes/dark'; import light from './themes/light'; -import '../src/styles/globals.scss'; type ThemeWrapperProps = { children: ReactNode; @@ -32,9 +33,16 @@ const withAllProviders: Decorator = (Story) => { enableColorScheme={true} enableSystem={true} > - <ThemeWrapper> - <Story /> - </ThemeWrapper> + <AckeeProvider + domainId="any" + server="https://example.com" + storageKey="ackee" + tracking="full" + > + <ThemeWrapper> + <Story /> + </ThemeWrapper> + </AckeeProvider> </ThemeProvider> </IntlProvider> ); diff --git a/src/components/organisms/forms/ackee-toggle/ackee-toggle.fixture.ts b/src/components/organisms/forms/ackee-toggle/ackee-toggle.fixture.ts deleted file mode 100644 index 04602f2..0000000 --- a/src/components/organisms/forms/ackee-toggle/ackee-toggle.fixture.ts +++ /dev/null @@ -1 +0,0 @@ -export const storageKey = 'ackee'; diff --git a/src/components/organisms/forms/ackee-toggle/ackee-toggle.stories.tsx b/src/components/organisms/forms/ackee-toggle/ackee-toggle.stories.tsx index 4122ed2..1b7b87b 100644 --- a/src/components/organisms/forms/ackee-toggle/ackee-toggle.stories.tsx +++ b/src/components/organisms/forms/ackee-toggle/ackee-toggle.stories.tsx @@ -1,6 +1,5 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { AckeeToggle } from './ackee-toggle'; -import { storageKey } from './ackee-toggle.fixture'; /** * AckeeToggle - Storybook Meta @@ -41,7 +40,4 @@ const Template: ComponentStory<typeof AckeeToggle> = (args) => ( * Toggle Stories - Ackee */ export const Ackee = Template.bind({}); -Ackee.args = { - defaultValue: 'full', - storageKey, -}; +Ackee.args = {}; diff --git a/src/components/organisms/forms/ackee-toggle/ackee-toggle.test.tsx b/src/components/organisms/forms/ackee-toggle/ackee-toggle.test.tsx index f7f5edf..68f8d19 100644 --- a/src/components/organisms/forms/ackee-toggle/ackee-toggle.test.tsx +++ b/src/components/organisms/forms/ackee-toggle/ackee-toggle.test.tsx @@ -1,12 +1,11 @@ import { describe, expect, it } from '@jest/globals'; import { render, screen as rtlScreen } from '../../../../../tests/utils'; import { AckeeToggle } from './ackee-toggle'; -import { storageKey } from './ackee-toggle.fixture'; describe('AckeeToggle', () => { // toHaveValue received undefined. Maybe because of localStorage hook... it('renders a toggle component', () => { - render(<AckeeToggle storageKey={storageKey} defaultValue="full" />); + render(<AckeeToggle />); expect( rtlScreen.getByRole('radiogroup', { name: /Tracking:/i, diff --git a/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx b/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx index a9c172b..9493095 100644 --- a/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx +++ b/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx @@ -1,11 +1,7 @@ /* eslint-disable max-statements */ -import { type ChangeEvent, type FC, useState, useCallback } from 'react'; +import { type FC, useState, useCallback } from 'react'; import { useIntl } from 'react-intl'; -import { - type AckeeOptions, - useLocalStorage, - useUpdateAckeeOptions, -} from '../../../../utils/hooks'; +import { useAckee } from '../../../../utils/hooks'; import { Legend, List, ListItem } from '../../../atoms'; import { Switch, @@ -15,49 +11,22 @@ import { type TooltipProps, } from '../../../molecules'; -const validator = (value: unknown): value is AckeeOptions => - value === 'full' || value === 'partial'; - export type AckeeToggleProps = Omit< SwitchProps, - 'isInline' | 'items' | 'name' | 'onSwitch' | 'value' + 'defaultValue' | 'isInline' | 'items' | 'name' | 'onSwitch' | 'value' > & - Pick<TooltipProps, 'direction'> & { - /** - * Set additional classnames to the toggle wrapper. - */ - className?: string; - /** - * True if motion should be reduced by default. - */ - defaultValue: AckeeOptions; - /** - * The local storage key to save preference. - */ - storageKey: string; - }; + Pick<TooltipProps, 'direction'>; /** * AckeeToggle component * * Render a Toggle component to set reduce motion. */ -export const AckeeToggle: FC<AckeeToggleProps> = ({ - defaultValue, - direction, - storageKey, - ...props -}) => { +export const AckeeToggle: FC<AckeeToggleProps> = ({ direction, ...props }) => { const intl = useIntl(); - const [value, setValue] = useLocalStorage( - storageKey, - defaultValue, - validator - ); + const [tracking, toggleTracking] = useAckee(); const [isTooltipOpened, setIsTooltipOpened] = useState(false); - useUpdateAckeeOptions(value); - const ackeeLabel = intl.formatMessage({ defaultMessage: 'Tracking:', description: 'AckeeToggle: select label', @@ -95,13 +64,6 @@ export const AckeeToggle: FC<AckeeToggleProps> = ({ { id: 'ackee-partial' as const, label: partialLabel, value: 'partial' }, ] satisfies [SwitchOption, SwitchOption]; - const updateSetting = useCallback( - (e: ChangeEvent<HTMLInputElement>) => { - setValue(e.target.value === 'full' ? 'full' : 'partial'); - }, - [setValue] - ); - const closeTooltip = useCallback(() => { setIsTooltipOpened(false); }, []); @@ -116,7 +78,7 @@ export const AckeeToggle: FC<AckeeToggleProps> = ({ items={options} legend={<Legend>{ackeeLabel}</Legend>} name="ackee" - onSwitch={updateSetting} + onSwitch={toggleTracking} tooltip={ <Tooltip direction={direction} @@ -134,7 +96,7 @@ export const AckeeToggle: FC<AckeeToggleProps> = ({ </List> </Tooltip> } - value={value} + value={tracking} /> ); }; diff --git a/src/components/organisms/modals/settings-modal.stories.tsx b/src/components/organisms/modals/settings-modal.stories.tsx index 7af0d60..57ce00f 100644 --- a/src/components/organisms/modals/settings-modal.stories.tsx +++ b/src/components/organisms/modals/settings-modal.stories.tsx @@ -1,6 +1,5 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { storageKey as ackeeStorageKey } from '../../organisms/forms/ackee-toggle/ackee-toggle.fixture'; -import { storageKey as motionStorageKey } from '../../organisms/forms/motion-toggle/motion-toggle.fixture'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { storageKey as motionStorageKey } from '../forms/motion-toggle/motion-toggle.fixture'; import { SettingsModal } from './settings-modal'; /** @@ -10,16 +9,6 @@ export default { title: 'Organisms/Modals', component: SettingsModal, argTypes: { - ackeeStorageKey: { - control: { - type: 'text', - }, - description: 'A local storage key for Ackee.', - type: { - name: 'string', - required: true, - }, - }, className: { control: { type: 'text', @@ -71,6 +60,5 @@ const Template: ComponentStory<typeof SettingsModal> = (args) => ( */ export const Settings = Template.bind({}); Settings.args = { - ackeeStorageKey, motionStorageKey, }; diff --git a/src/components/organisms/modals/settings-modal.test.tsx b/src/components/organisms/modals/settings-modal.test.tsx index bb0cdf2..26d046a 100644 --- a/src/components/organisms/modals/settings-modal.test.tsx +++ b/src/components/organisms/modals/settings-modal.test.tsx @@ -1,41 +1,30 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { storageKey as ackeeStorageKey } from '../../organisms/forms/ackee-toggle/ackee-toggle.fixture'; -import { storageKey as motionStorageKey } from '../../organisms/forms/motion-toggle/motion-toggle.fixture'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; +import { storageKey as motionStorageKey } from '../forms/motion-toggle/motion-toggle.fixture'; import { SettingsModal } from './settings-modal'; describe('SettingsModal', () => { it('renders the modal heading', () => { - render( - <SettingsModal - ackeeStorageKey={ackeeStorageKey} - motionStorageKey={motionStorageKey} - /> - ); - expect(screen.getByText(/Settings/i)).toBeInTheDocument(); + render(<SettingsModal motionStorageKey={motionStorageKey} />); + expect(rtlScreen.getByText(/Settings/i)).toBeInTheDocument(); }); it('renders a settings form', () => { - render( - <SettingsModal - ackeeStorageKey={ackeeStorageKey} - motionStorageKey={motionStorageKey} - /> - ); + render(<SettingsModal motionStorageKey={motionStorageKey} />); expect( - screen.getByRole('form', { name: /^Settings form/i }) + rtlScreen.getByRole('form', { name: /^Settings form/i }) ).toBeInTheDocument(); expect( - screen.getByRole('radiogroup', { name: /^Theme:/i }) + rtlScreen.getByRole('radiogroup', { name: /^Theme:/i }) ).toBeInTheDocument(); expect( - screen.getByRole('radiogroup', { name: /^Code blocks:/i }) + rtlScreen.getByRole('radiogroup', { name: /^Code blocks:/i }) ).toBeInTheDocument(); expect( - screen.getByRole('radiogroup', { name: /^Animations:/i }) + rtlScreen.getByRole('radiogroup', { name: /^Animations:/i }) ).toBeInTheDocument(); expect( - screen.getByRole('radiogroup', { name: /^Tracking:/i }) + rtlScreen.getByRole('radiogroup', { name: /^Tracking:/i }) ).toBeInTheDocument(); }); }); diff --git a/src/components/organisms/modals/settings-modal.tsx b/src/components/organisms/modals/settings-modal.tsx index 5a53bbd..f62312b 100644 --- a/src/components/organisms/modals/settings-modal.tsx +++ b/src/components/organisms/modals/settings-modal.tsx @@ -3,7 +3,6 @@ import { useIntl } from 'react-intl'; import { Form, Heading, Icon, Modal, type ModalProps } from '../../atoms'; import { AckeeToggle, - type AckeeToggleProps, MotionToggle, type MotionToggleProps, PrismThemeToggle, @@ -13,10 +12,6 @@ import styles from './settings-modal.module.scss'; export type SettingsModalProps = Pick<ModalProps, 'className'> & { /** - * The local storage key for Ackee settings. - */ - ackeeStorageKey: AckeeToggleProps['storageKey']; - /** * The local storage key for Reduce motion settings. */ motionStorageKey: MotionToggleProps['storageKey']; @@ -29,7 +24,6 @@ export type SettingsModalProps = Pick<ModalProps, 'className'> & { */ export const SettingsModal: FC<SettingsModalProps> = ({ className = '', - ackeeStorageKey, motionStorageKey, }) => { const intl = useIntl(); @@ -70,12 +64,7 @@ export const SettingsModal: FC<SettingsModalProps> = ({ defaultValue="on" storageKey={motionStorageKey} /> - <AckeeToggle - className={styles.item} - direction="upwards" - defaultValue="full" - storageKey={ackeeStorageKey} - /> + <AckeeToggle className={styles.item} direction="upwards" /> </Form> </Modal> ); diff --git a/src/components/organisms/toolbar/settings.stories.tsx b/src/components/organisms/toolbar/settings.stories.tsx index bea0d9e..66b4e0f 100644 --- a/src/components/organisms/toolbar/settings.stories.tsx +++ b/src/components/organisms/toolbar/settings.stories.tsx @@ -1,5 +1,5 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { useState } from 'react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { useCallback, useState } from 'react'; import { Settings } from './settings'; /** @@ -9,20 +9,9 @@ export default { title: 'Organisms/Toolbar/Settings', component: Settings, args: { - ackeeStorageKey: 'ackee-tracking', motionStorageKey: 'reduced-motion', }, argTypes: { - ackeeStorageKey: { - control: { - type: 'text', - }, - description: 'Set Ackee settings local storage key.', - type: { - name: 'string', - required: true, - }, - }, className: { control: { type: 'text', @@ -92,15 +81,11 @@ const Template: ComponentStory<typeof Settings> = ({ }) => { const [isOpen, setIsOpen] = useState<boolean>(isActive); - return ( - <Settings - isActive={isOpen} - setIsActive={() => { - setIsOpen(!isOpen); - }} - {...args} - /> - ); + const toggle = useCallback(() => { + setIsOpen((prevState) => !prevState); + }, []); + + return <Settings isActive={isOpen} setIsActive={toggle} {...args} />; }; /** diff --git a/src/components/organisms/toolbar/settings.test.tsx b/src/components/organisms/toolbar/settings.test.tsx index 9dab407..66fa6a6 100644 --- a/src/components/organisms/toolbar/settings.test.tsx +++ b/src/components/organisms/toolbar/settings.test.tsx @@ -1,33 +1,35 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; import { Settings } from './settings'; +const doNothing = () => { + // do nothing +}; + describe('Settings', () => { it('renders a button to open settings modal', () => { render( <Settings - ackeeStorageKey="ackee-tracking" motionStorageKey="reduced-motion" isActive={false} - setIsActive={() => null} + setIsActive={doNothing} /> ); expect( - screen.getByRole('checkbox', { name: 'Open settings' }) + rtlScreen.getByRole('checkbox', { name: 'Open settings' }) ).toBeInTheDocument(); }); it('renders a button to close settings modal', () => { render( <Settings - ackeeStorageKey="ackee-tracking" motionStorageKey="reduced-motion" isActive={true} - setIsActive={() => null} + setIsActive={doNothing} /> ); expect( - screen.getByRole('checkbox', { name: 'Close settings' }) + rtlScreen.getByRole('checkbox', { name: 'Close settings' }) ).toBeInTheDocument(); }); }); diff --git a/src/components/organisms/toolbar/settings.tsx b/src/components/organisms/toolbar/settings.tsx index b7625aa..124dd42 100644 --- a/src/components/organisms/toolbar/settings.tsx +++ b/src/components/organisms/toolbar/settings.tsx @@ -20,13 +20,7 @@ const SettingsWithRef: ForwardRefRenderFunction< HTMLDivElement, SettingsProps > = ( - { - ackeeStorageKey, - className = '', - isActive = false, - motionStorageKey, - setIsActive, - }, + { className = '', isActive = false, motionStorageKey, setIsActive }, ref ) => { const intl = useIntl(); @@ -61,7 +55,6 @@ const SettingsWithRef: ForwardRefRenderFunction< label={label} /> <SettingsModal - ackeeStorageKey={ackeeStorageKey} className={`${styles.modal} ${className}`} motionStorageKey={motionStorageKey} /> diff --git a/src/components/organisms/toolbar/toolbar.stories.tsx b/src/components/organisms/toolbar/toolbar.stories.tsx index 7bf545b..22bead9 100644 --- a/src/components/organisms/toolbar/toolbar.stories.tsx +++ b/src/components/organisms/toolbar/toolbar.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { Toolbar as ToolbarComponent } from './toolbar'; /** @@ -8,21 +8,10 @@ export default { title: 'Organisms/Toolbar', component: ToolbarComponent, args: { - ackeeStorageKey: 'ackee-tracking', motionStorageKey: 'reduced-motion', searchPage: '#', }, argTypes: { - ackeeStorageKey: { - control: { - type: 'text', - }, - description: 'Set Ackee settings local storage key.', - type: { - name: 'string', - required: true, - }, - }, className: { control: { type: 'text', diff --git a/src/components/organisms/toolbar/toolbar.test.tsx b/src/components/organisms/toolbar/toolbar.test.tsx index 8fb06b0..e6b1022 100644 --- a/src/components/organisms/toolbar/toolbar.test.tsx +++ b/src/components/organisms/toolbar/toolbar.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; import { Toolbar } from './toolbar'; const nav = [ @@ -12,13 +12,8 @@ const nav = [ describe('Toolbar', () => { it('renders a navigation menu', () => { render( - <Toolbar - ackeeStorageKey="ackee-tracking" - motionStorageKey="reduced-motion" - nav={nav} - searchPage="#" - /> + <Toolbar motionStorageKey="reduced-motion" nav={nav} searchPage="#" /> ); - expect(screen.getByRole('navigation')).toBeInTheDocument(); + expect(rtlScreen.getByRole('navigation')).toBeInTheDocument(); }); }); diff --git a/src/components/organisms/toolbar/toolbar.tsx b/src/components/organisms/toolbar/toolbar.tsx index 999a29a..be46636 100644 --- a/src/components/organisms/toolbar/toolbar.tsx +++ b/src/components/organisms/toolbar/toolbar.tsx @@ -7,7 +7,7 @@ import { Settings, type SettingsProps } from './settings'; import styles from './toolbar.module.scss'; export type ToolbarProps = Pick<SearchProps, 'searchPage'> & - Pick<SettingsProps, 'ackeeStorageKey' | 'motionStorageKey'> & { + Pick<SettingsProps, 'motionStorageKey'> & { /** * Set additional classnames to the toolbar wrapper. */ @@ -24,7 +24,6 @@ export type ToolbarProps = Pick<SearchProps, 'searchPage'> & * Render the website toolbar. */ export const Toolbar: FC<ToolbarProps> = ({ - ackeeStorageKey, className = '', motionStorageKey, nav, @@ -76,7 +75,6 @@ export const Toolbar: FC<ToolbarProps> = ({ setIsActive={toggleSearch} /> <Settings - ackeeStorageKey={ackeeStorageKey} className={`${styles.modal} ${styles['modal--settings']}`} isActive={isSettingsOpened} motionStorageKey={motionStorageKey} diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx index bf9629c..fd3a928 100644 --- a/src/components/templates/layout/layout.tsx +++ b/src/components/templates/layout/layout.tsx @@ -312,8 +312,6 @@ export const Layout: FC<LayoutProps> = ({ url="/" /> <Toolbar - // eslint-disable-next-line react/jsx-no-literals -- Storage key allowed - ackeeStorageKey="ackee-tracking" className={styles.toolbar} // eslint-disable-next-line react/jsx-no-literals -- Storage key allowed motionStorageKey="reduced-motion" diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 0fb17f4..914b0b6 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,19 +1,25 @@ -import { ThemeProvider } from 'next-themes'; import { useRouter } from 'next/router'; +import { ThemeProvider } from 'next-themes'; import { IntlProvider } from 'react-intl'; import '../styles/globals.scss'; -import { type AppPropsWithLayout } from '../types'; +import type { AppPropsWithLayout } from '../types'; import { settings } from '../utils/config'; +import { STORAGE_KEY } from '../utils/constants'; import { AckeeProvider, PrismThemeProvider } from '../utils/providers'; const App = ({ Component, pageProps }: AppPropsWithLayout) => { const { locale, defaultLocale } = useRouter(); - const appLocale: string = locale || settings.locales.defaultLocale; + const appLocale: string = locale ?? settings.locales.defaultLocale; const getLayout = Component.getLayout ?? ((page) => page); const { translation, ...componentProps } = pageProps; return ( - <AckeeProvider domain={settings.ackee.url} siteId={settings.ackee.siteId}> + <AckeeProvider + domainId={settings.ackee.siteId} + server={settings.ackee.url} + storageKey={STORAGE_KEY.ACKEE} + tracking="full" + > <IntlProvider locale={appLocale} defaultLocale={defaultLocale} diff --git a/src/types/app.ts b/src/types/app.ts index 93ba1db..565fe97 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -137,3 +137,5 @@ export type Position = 'bottom' | 'center' | 'left' | 'right' | 'top'; export type Spacing = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'; export type Validator<T> = (value: unknown) => value is T; + +export type AckeeTrackerValue = 'full' | 'partial'; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index e642af9..464db3f 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -30,3 +30,7 @@ export const ROUTES = { } as const; // cSpell:ignore legales thematique developpement + +export const STORAGE_KEY = { + ACKEE: 'ackee-tracking', +} as const; diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index 47e90f1..cf8c01c 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -1,3 +1,4 @@ +export * from './use-ackee'; export * from './use-add-classname'; export * from './use-article'; export * from './use-attributes'; @@ -20,4 +21,3 @@ export * from './use-route-change'; export * from './use-scroll-position'; export * from './use-settings'; export * from './use-state-change'; -export * from './use-update-ackee-options'; diff --git a/src/utils/hooks/use-ackee/index.ts b/src/utils/hooks/use-ackee/index.ts new file mode 100644 index 0000000..81cac12 --- /dev/null +++ b/src/utils/hooks/use-ackee/index.ts @@ -0,0 +1 @@ +export * from './use-ackee'; diff --git a/src/utils/hooks/use-ackee/use-ackee.test.tsx b/src/utils/hooks/use-ackee/use-ackee.test.tsx new file mode 100644 index 0000000..230fe0b --- /dev/null +++ b/src/utils/hooks/use-ackee/use-ackee.test.tsx @@ -0,0 +1,44 @@ +import { act, renderHook } from '@testing-library/react'; +import type { FC, ReactNode } from 'react'; +import type { AckeeTrackerValue } from '../../../types'; +import { AckeeProvider, type AckeeProviderProps } from '../../providers'; +import { useAckee } from './use-ackee'; + +const createWrapper = ( + Wrapper: FC<AckeeProviderProps>, + config: AckeeProviderProps +) => + function CreatedWrapper({ children }: { children: ReactNode }) { + return <Wrapper {...config}>{children}</Wrapper>; + }; + +describe('useAckee', () => { + it('should return the default value without provider and prevent update', () => { + const { result } = renderHook(() => useAckee()); + + expect(result.current[0]).toBe('full'); + + act(() => result.current[1]()); + + expect(result.current[0]).toBe('full'); + }); + + it('can update the value', () => { + const defaultValue: AckeeTrackerValue = 'full'; + + const { result } = renderHook(() => useAckee(), { + wrapper: createWrapper(AckeeProvider, { + domainId: 'some-id', + server: 'https://example.com', + storageKey: 'veniam', + tracking: defaultValue, + }), + }); + + expect(result.current[0]).toBe(defaultValue); + + act(() => result.current[1]()); + + expect(result.current[0]).toBe('partial'); + }); +}); diff --git a/src/utils/hooks/use-ackee/use-ackee.ts b/src/utils/hooks/use-ackee/use-ackee.ts new file mode 100644 index 0000000..a89701a --- /dev/null +++ b/src/utils/hooks/use-ackee/use-ackee.ts @@ -0,0 +1,15 @@ +import { useCallback, useContext } from 'react'; +import { AckeeContext } from '../../providers'; + +export const useAckee = () => { + const { tracking, setTracking } = useContext(AckeeContext); + + const toggle = useCallback(() => { + setTracking((prev) => { + if (prev === 'full') return 'partial'; + return 'full'; + }); + }, [setTracking]); + + return [tracking, toggle] as const; +}; diff --git a/src/utils/hooks/use-update-ackee-options.tsx b/src/utils/hooks/use-update-ackee-options.tsx deleted file mode 100644 index f6e5c86..0000000 --- a/src/utils/hooks/use-update-ackee-options.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useEffect } from 'react'; -import { useAckeeTracker } from '../providers'; - -export type AckeeOptions = 'full' | 'partial'; - -/** - * Update Ackee settings with the given choice. - * - * @param {AckeeOptions} value - Either `full` or `partial`. - */ -export const useUpdateAckeeOptions = (value: AckeeOptions) => { - const { setDetailed } = useAckeeTracker(); - - useEffect(() => { - setDetailed(value === 'full'); - }, [value, setDetailed]); -}; diff --git a/src/utils/providers/ackee-provider/ackee-provider.test.tsx b/src/utils/providers/ackee-provider/ackee-provider.test.tsx new file mode 100644 index 0000000..7ba11dc --- /dev/null +++ b/src/utils/providers/ackee-provider/ackee-provider.test.tsx @@ -0,0 +1,46 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { type FC, useContext } from 'react'; +import type { AckeeTrackerValue } from '../../../types'; +import { AckeeContext, AckeeProvider } from './ackee-provider'; + +const bodyPrefix = 'Tracking is set to:'; + +const ComponentTest: FC = () => { + const { tracking } = useContext(AckeeContext); + + return ( + <div> + {bodyPrefix} {tracking} + </div> + ); +}; + +describe('AckeeProvider', () => { + it('uses the default value when the provider is not used', () => { + render(<ComponentTest />); + + expect(rtlScreen.getByText(new RegExp(bodyPrefix))).toHaveTextContent( + `${bodyPrefix} full` + ); + }); + + it('provides the given tracking value to its children', () => { + const tracking: AckeeTrackerValue = 'partial'; + + render( + <AckeeProvider + domainId="some-id" + server="https://example.com" + storageKey="facilis" + tracking={tracking} + > + <ComponentTest /> + </AckeeProvider> + ); + + expect(rtlScreen.getByText(new RegExp(bodyPrefix))).toHaveTextContent( + `${bodyPrefix} ${tracking}` + ); + }); +}); diff --git a/src/utils/providers/ackee-provider/ackee-provider.tsx b/src/utils/providers/ackee-provider/ackee-provider.tsx new file mode 100644 index 0000000..8cd29cd --- /dev/null +++ b/src/utils/providers/ackee-provider/ackee-provider.tsx @@ -0,0 +1,95 @@ +import { useRouter } from 'next/router'; +import { + type FC, + type ReactNode, + createContext, + type Dispatch, + type SetStateAction, + useMemo, +} from 'react'; +import { useAckee } from 'use-ackee'; +import type { AckeeTrackerValue } from '../../../types'; +import { useLocalStorage } from '../../hooks'; + +type AckeeContextProps = { + tracking: AckeeTrackerValue; + setTracking: Dispatch<SetStateAction<AckeeTrackerValue>>; +}; + +export const AckeeContext = createContext<AckeeContextProps>({ + setTracking: (value) => value, + tracking: 'full', +}); + +const validator = (value: unknown): value is AckeeTrackerValue => + value === 'full' || value === 'partial'; + +export type AckeeProviderProps = { + /** + * The provider children. + */ + children?: ReactNode; + /** + * The id given by Ackee for this domain. + */ + domainId: string; + /** + * Should we track visits from localhost? + * + * @default false + */ + isLocalhostTracked?: boolean; + /** + * Should we track our own visits? + * + * @default false + */ + isOwnVisitsTracked?: boolean; + /** + * An URL that points to your Ackee installation (without trailing slash). + */ + server: string; + /** + * The key to use in local storage. + */ + storageKey: string; + /** + * Should the tracking be detailed (full) or not (partial)? + */ + tracking: AckeeTrackerValue; +}; + +export const AckeeProvider: FC<AckeeProviderProps> = ({ + children, + domainId, + isLocalhostTracked = false, + isOwnVisitsTracked = false, + server, + storageKey, + tracking: tracker, +}) => { + const [tracking, setTracking] = useLocalStorage<AckeeTrackerValue>( + storageKey, + tracker, + validator + ); + const { asPath } = useRouter(); + + useAckee( + asPath, + { domainId, server }, + { + detailed: tracking === 'full', + ignoreLocalhost: !isLocalhostTracked, + ignoreOwnVisits: !isOwnVisitsTracked, + } + ); + + const value = useMemo(() => { + return { setTracking, tracking }; + }, [setTracking, tracking]); + + return ( + <AckeeContext.Provider value={value}>{children}</AckeeContext.Provider> + ); +}; diff --git a/src/utils/providers/ackee-provider/index.ts b/src/utils/providers/ackee-provider/index.ts new file mode 100644 index 0000000..10f7a26 --- /dev/null +++ b/src/utils/providers/ackee-provider/index.ts @@ -0,0 +1 @@ +export * from './ackee-provider'; diff --git a/src/utils/providers/ackee.tsx b/src/utils/providers/ackee.tsx deleted file mode 100644 index 0cb0166..0000000 --- a/src/utils/providers/ackee.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useRouter } from 'next/router'; -import { createContext, FC, ReactNode, useContext, useState } from 'react'; -import useAckee from 'use-ackee'; - -export type AckeeProps = { - domain: string; - siteId: string; - detailed?: boolean; - setDetailed: (isDetailed: boolean) => void; -}; - -export type AckeeProviderProps = { - children: ReactNode; - domain: string; - siteId: string; - ignoreLocalhost?: boolean; - ignoreOwnVisits?: boolean; -}; - -export const AckeeContext = createContext<AckeeProps>({ - domain: '', - siteId: '', - setDetailed: (_) => { - // Do nothing. - }, -}); - -export const useAckeeTracker = () => useContext(AckeeContext); - -export const AckeeProvider: FC<AckeeProviderProps> = ({ - domain, - siteId, - children, - ignoreLocalhost = true, - ignoreOwnVisits = true, -}) => { - const [detailed, setDetailed] = useState<boolean>(false); - const { asPath } = useRouter(); - - useAckee( - asPath, - { server: domain, domainId: siteId }, - { detailed, ignoreLocalhost, ignoreOwnVisits } - ); - - return ( - <AckeeContext.Provider - value={{ - domain, - siteId, - detailed, - setDetailed, - }} - > - {children} - </AckeeContext.Provider> - ); -}; diff --git a/src/utils/providers/index.ts b/src/utils/providers/index.ts index 43641a1..640730f 100644 --- a/src/utils/providers/index.ts +++ b/src/utils/providers/index.ts @@ -1,2 +1,2 @@ -export * from './ackee'; +export * from './ackee-provider'; export * from './prism-theme'; diff --git a/tests/utils/index.tsx b/tests/utils/index.tsx index 1bcea8e..8766318 100644 --- a/tests/utils/index.tsx +++ b/tests/utils/index.tsx @@ -1,7 +1,11 @@ -import { render, RenderOptions } from '@testing-library/react'; +import { + render as rtlRender, + type RenderOptions, +} from '@testing-library/react'; import { ThemeProvider } from 'next-themes'; -import { FC, ReactElement, ReactNode } from 'react'; +import type { FC, ReactElement, ReactNode } from 'react'; import { IntlProvider } from 'react-intl'; +import { AckeeProvider } from '../../src/utils/providers'; type ProvidersConfig = { children: ReactNode; @@ -18,13 +22,20 @@ type CustomRenderOptions = { * * @returns A component wrapped Intl and Theme providers. */ -const AllTheProviders: FC<ProvidersConfig> = ({ children, locale = 'en' }) => { - return ( - <IntlProvider locale={locale}> - <ThemeProvider>{children}</ThemeProvider> - </IntlProvider> - ); -}; +const AllTheProviders: FC<ProvidersConfig> = ({ children, locale = 'en' }) => ( + <IntlProvider locale={locale}> + <ThemeProvider> + <AckeeProvider + domainId="any-id" + server="https://example.test" + storageKey="ackee" + tracking="full" + > + {children} + </AckeeProvider> + </ThemeProvider> + </IntlProvider> +); /** * Render a component with all the providers. @@ -33,11 +44,12 @@ const AllTheProviders: FC<ProvidersConfig> = ({ children, locale = 'en' }) => { * @param {CustomRenderOptions} [options] - An object of render options and providers options. * @returns A React component wrapped with all the providers. */ -const customRender = (ui: ReactElement, options?: CustomRenderOptions) => - render(ui, { +const render = (ui: ReactElement, options?: CustomRenderOptions) => + rtlRender(ui, { wrapper: (props) => <AllTheProviders {...props} {...options?.providers} />, ...options?.testingLibrary, }); +/* eslint-disable import/export */ export * from '@testing-library/react'; -export { customRender as render }; +export { render }; |
