Skip to content

Commit

Permalink
feat: components, hooks, lib
Browse files Browse the repository at this point in the history
  • Loading branch information
reapziq committed May 1, 2023
1 parent 2c54dc5 commit 78ea3ea
Show file tree
Hide file tree
Showing 15 changed files with 1,033 additions and 19 deletions.
1 change: 1 addition & 0 deletions jest-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom';
6 changes: 6 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
clearMocks: true,
setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
transform: {
'^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
},
transformIgnorePatterns: ['/node_modules/'],
};

export default config;
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@
"scripts": {
"lint": "eslint src --fix",
"build": "del-cli dist && tsc",
"prepublishOnly": "yarn build",
"test": "jest"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.5.1",
"@types/react": "^18.2.0",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
Expand All @@ -34,10 +38,13 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"next": "^13.3.2",
"prettier": "^2.8.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
},
"peerDependencies": {
Expand Down
15 changes: 15 additions & 0 deletions src/components/MetricaPixel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, { FC } from 'react';

interface Props {
tagID: number;
}

export const MetricaPixel: FC<Props> = ({ tagID }) => (
<img
height="1"
width="1"
style={{ display: 'none' }}
src={`https://mc.yandex.ru/watch/${tagID}`}
alt=""
/>
);
54 changes: 54 additions & 0 deletions src/components/YandexMetricaProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Script, { ScriptProps } from 'next/script';
import React, { createContext, FC, ReactNode, useMemo } from 'react';

import { useTrackRouteChange } from '../hooks/useTrackRouteChange';
import { InitParameters } from '../lib/types/parameters';
import { MetricaPixel } from './MetricaPixel';

export const MetricaTagIDContext = createContext<number | null>(null);

interface Props {
children: ReactNode;
tagID?: number;
strategy?: ScriptProps['strategy'];
initParameters?: InitParameters;
}

export const YandexMetricaProvider: FC<Props> = ({
children,
tagID,
strategy = 'afterInteractive',
initParameters,
}) => {
const YANDEX_METRICA_ID = process.env.NEXT_PUBLIC_YANDEX_METRICA_ID;
const id = useMemo(() => {
return tagID || (YANDEX_METRICA_ID ? Number(YANDEX_METRICA_ID) : null);
}, [YANDEX_METRICA_ID, tagID]);

useTrackRouteChange({ tagID: id });

if (!id) {
console.warn('[next-yandex-metrica] Yandex.Metrica tag ID is not defined');

return <>{children}</>;
}

return (
<>
<Script id="yandex-metrica" strategy={strategy}>
{`
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(${id}, "init", ${JSON.stringify(initParameters || {})});
`}
</Script>
<noscript id="yandex-metrica-pixel">
<MetricaPixel tagID={id} />
</noscript>
<MetricaTagIDContext.Provider value={id}>{children}</MetricaTagIDContext.Provider>
</>
);
};
48 changes: 48 additions & 0 deletions src/hooks/useMetrica.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useCallback, useContext } from 'react';

import { MetricaTagIDContext } from '../components/YandexMetricaProvider';
import { type EventParameters } from '../lib/types/events';
import { type NotBounceOptions } from '../lib/types/options';
import { type UserParameters, type VisitParameters } from '../lib/types/parameters';
import { ym } from '../lib/ym';

export const useMetrica = () => {
const tagID = useContext(MetricaTagIDContext);

const notBounce = useCallback(
(options?: NotBounceOptions) => {
ym(tagID, 'notBounce', options);
},
[tagID],
);

const reachGoal = useCallback(
(target: string, params?: VisitParameters, callback?: () => void) => {
ym(tagID, 'reachGoal', target, params, callback);
},
[tagID],
);

const setUserID = useCallback(
(userID: string) => {
ym(tagID, 'setUserID', userID);
},
[tagID],
);

const userParams = useCallback(
(parameters: UserParameters) => {
ym(tagID, 'userParams', parameters);
},
[tagID],
);

const ymEvent = useCallback(
(...parameters: EventParameters) => {
ym(tagID, ...parameters);
},
[tagID],
);

return { notBounce, reachGoal, setUserID, userParams, ymEvent };
};
18 changes: 18 additions & 0 deletions src/hooks/useTrackRouteChange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Router } from 'next/router';
import { useEffect } from 'react';

import { ym } from '../lib/ym';

export const useTrackRouteChange = ({ tagID }: { tagID: number | null }) => {
useEffect(() => {
const handleRouteChange = (url: URL): void => {
ym(tagID, 'hit', url.toString());
};

Router.events.on('routeChangeComplete', handleRouteChange);

return () => {
Router.events.off('routeChangeComplete', handleRouteChange);
};
}, [tagID]);
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { YandexMetricaProvider } from './components/YandexMetricaProvider';
export { useMetrica } from './hooks/useMetrica';
56 changes: 56 additions & 0 deletions src/lib/types/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ExtLinkOptions, FileOptions, HitOptions, NotBounceOptions } from './options';
import {
FirstPartyParamsParameters,
InitParameters,
UserParameters,
VisitParameters,
} from './parameters';

type InitEventParameters = [eventName: 'init', parameters: InitParameters];

type AddFileExtensionEventParameters = [
eventName: 'addFileExtension',
extensions: string | string[],
];

type ExtLinkEventParameters = [eventName: 'extLink', url: string, options?: ExtLinkOptions];

type FileEventParameters = [eventName: 'file', url: string, options?: FileOptions];

type FirstPartyParamsEventParameters = [
eventName: 'firstPartyParams',
parameters: FirstPartyParamsParameters,
];

type GetClientIDEventParameters = [eventName: 'getClientID', cb: (clientID: string) => void];

type HitEventParameters = [eventName: 'hit', url: string, options?: HitOptions];

type NotBounceEventParameters = [eventName: 'notBounce', options?: NotBounceOptions];

type ParamsEventParameters = [eventName: 'params', parameters: VisitParameters | VisitParameters[]];

type ReachGoalEventParameters = [
eventName: 'reachGoal',
target: string,
params?: VisitParameters,
callback?: () => void,
];

type SetUserIDEventParameters = [eventName: 'setUserID', userID: string];

type UserParamsEventParameters = [eventName: 'userParams', parameters: UserParameters];

export type EventParameters =
| InitEventParameters
| AddFileExtensionEventParameters
| ExtLinkEventParameters
| FileEventParameters
| FirstPartyParamsEventParameters
| GetClientIDEventParameters
| HitEventParameters
| NotBounceEventParameters
| ParamsEventParameters
| ReachGoalEventParameters
| SetUserIDEventParameters
| UserParamsEventParameters;
25 changes: 25 additions & 0 deletions src/lib/types/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { VisitParameters } from './parameters';

export interface ExtLinkOptions {
callback?: () => void;
params?: VisitParameters;
title?: string;
}

export interface FileOptions {
callback?: () => void;
params?: VisitParameters;
referer?: string;
title?: string;
}

export interface HitOptions {
callback?: () => void;
params?: VisitParameters;
referer?: string;
title?: string;
}

export interface NotBounceOptions {
callback?: () => void;
}
43 changes: 43 additions & 0 deletions src/lib/types/parameters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export interface VisitParameters {
order_price?: number;
currency?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}

export interface UserParameters {
UserID?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}

export interface InitParameters {
accurateTrackBounce?: boolean | number;
childIframe?: boolean;
clickmap?: boolean;
defer?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ecommerce?: boolean | string | any[];
params?: VisitParameters | VisitParameters[];
userParams?: UserParameters;
trackHash?: boolean;
trackLinks?: boolean;
trustedDomains?: string[];
type?: number;
webvisor?: boolean;
triggerEvent?: boolean;
}

export interface FirstPartyParamsParameters {
email?: string;
phone_number?: string;
first_name?: string;
last_name?: string;
home_address?: string;
street?: string;
city?: string;
region?: string;
postal_code?: string;
country?: string;
yandex_cid?: number;
}
3 changes: 3 additions & 0 deletions src/lib/types/ym.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { EventParameters } from './events';

export type YM = (tagID: number, ...parameters: EventParameters) => void;
14 changes: 14 additions & 0 deletions src/lib/ym.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { EventParameters } from './types/events';
import { YM } from './types/ym';

export const ym = (tagID: number | null, ...parameters: EventParameters) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- ym is defined by the Yandex.Metrica script
// @ts-ignore
const ym = window.ym as YM | undefined;

if (!ym || !tagID) {
return;
}

ym(tagID, ...parameters);
};
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
"jsx": "react"
},
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"exclude": ["./src/**/*.test.ts"]
"exclude": ["./src/**/*.test.ts", "./src/**/*.test.tsx"]
}
Loading

0 comments on commit 78ea3ea

Please sign in to comment.