diff --git a/.eslintrc b/.eslintrc index 19f5c0b80..0138c2e29 100644 --- a/.eslintrc +++ b/.eslintrc @@ -69,11 +69,15 @@ "linebreak-style": "off", "no-debugger": "off", "no-alert": "off", - "no-use-before-define": "warn", - "no-unused-vars": [ + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": ["error"], + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ "warn", { - "argsIgnorePattern": "res|next|^err" + "argsIgnorePattern": "res|next|^err|^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" } ], "prefer-const": [ @@ -110,20 +114,8 @@ "consistent-return": "off", "jsx-a11y/accessible-emoji": "off", "radix": "off", - "no-shadow": [ - 2, - { - "hoist": "all", - "allow": [ - "resolve", - "reject", - "done", - "next", - "err", - "error" - ] - } - ], + "no-shadow": "off", + "@typescript-eslint/no-shadow": ["warn"], "quotes": [ 2, "single", diff --git a/CHANGELOG.md b/CHANGELOG.md index 3852ea521..b56da700e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,193 @@ All notable changes to this project will be documented in this file. +# [2.5.28] - 29.09.2022 + +## Added + +- Add `disabled` for Dropdown options +# [2.5.27] - 04.08.2022 + +## Added + +- Changed the color of the text in the footer for Previews from fixed value to a value from the theme colors + +# [2.5.26] - 21.07.2022 + +## Added + +- Add `hasItemEntries` prop for Table component + +# [2.5.25] - 04.04.2022 + +## Added + +- Add `displayFormat` prop for DayPicker component + +# [2.5.24] - 09.03.2022 + +## Added + +- New `useAnalytics` hook as an alternative to `AnalyticsComponent` + +# [2.5.23] - 21.02.2022 + +## Change + +- Buy button tag name on paywall previews + +# [2.5.22] - 04.02.2022 + +## Added + +- Option in column table to make fields(columns) editable +- onBlur function to trigger persa when input and textarea are focused-out + +# [2.5.21] - 25.01.2022 + +## Change + +- Styling changes in preview templates for restricted asset +- FooterText component refactored + +# [2.5.20] - 21.01.2022 + +## Fixes + +- TextArea by providing the required props + +# [2.5.19] - 21.01.2022 + +## Added + +- Handlers for Input, TextArea, Pagination, Accordion and Switch. +- Tagging Paywall previews + +## Fixes + +- TabNavigation handler type + +# [2.5.18] - 10.01.2022 + +## Added + +- Handlers for Checkbox, Radiobutton, Daypicker and Datepicker. +- Handler for selected Tab change. + +# [2.5.17] - 12.11.2021 + +## Added + +- Tagging for StepWizard + +## Fixes + +- Dropdown component tag sent in tracker + +# [2.5.16] - 12.11.2021 + +## Added + +- Analytics handler for dropdown component +- Tagging for pagination and navbar + +# [2.5.15] - 02.11.2021 + +## Fixes + +- Tab component import bug + +### Change + +- Accordion component to be analytics ready + +# [2.5.14] - 27.10.2021 + +## Added + +- Analytics handler for button component + +# [2.5.13] - 05-10-2021 + +## Added + +- AnalyticsProps type to the components in index.d.ts file. + +# [2.5.12] - 28-09-2021 + +## Added + +- Explicit exports for components, types and interfaces from `src/analytics`. + +# [2.5.11] - 28-09-2021 + +## Added + +- Analytics tracking adds forgotten import for React. + +# [2.5.10] - 27-09-2021 + +### Added + +- Analytics tracking props for all components. + +# [2.5.9] - 23-09-2021 + +### Added + +- Add another type of datepicker component with dropdown presets +- Drawer component functionality and design + +# [2.5.8] - 19-08-2021 + +### Change + +- Return Accordion panel label when toggling it. + +# [2.5.7] - 17-06-2021 + +### Change + +- Changes connected with logo re-branding + +# [2.5.6] - 09-04-2021 + +### Fixes + +- Changed width, height and padding-left of Checkbox component's top wrapper to be in `em` instead of `rem` + +# [2.5.5] - 22-03-2021 + +### Change + +- Add styling changes for asset restrictions in preview templates + +# [2.5.4] - 12-03-2021 + +### Change + +- Add missing className for Switch component in order to externaly style it with styled-components + +# [2.5.3] - 23-02-2021 + +### Fixes + +- Daypicker moment type error + +# [2.5.2] - 22-01-2021 + +### Change + +- Use pixels instead of rem for font sizes + +# [2.5.1] - 15-01-2021 + +### Added + +- Add the possibility to have more than one table button + # [2.5.0] - 12-01-2021 + ### Changes - Change footer text depending on user auth state diff --git a/index.d.ts b/index.d.ts index 4570f85ef..d9ddbd48b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -19,7 +19,113 @@ import { FocusedInputShape } from 'react-dates'; export { FocusedInputShape } from 'react-dates'; -export interface ContainerProps { +export type AnalyticsTag = string; + +/** + * Props present on various components which aid in the tracking of analytics events. + */ +export interface AnalyticsProps { + /** Unique identifier of the component within the logical page hierarchy. */ + tag?: AnalyticsTag; + + /** Posiition of the component within a table-like component. */ + position?: { + /** Unique ID of the row (usually what you would use as a key in react). */ + id: string; + + /** Row index. */ + row?: number; + + /** Column index or name. */ + column?: number | string; + }; +} + +export declare class AnalyticsTracker { + registerHandler: (fn: AnalyticsHandlerFn) => void; + deregisterHandler: (fn: AnalyticsHandlerFn) => void; + track: (event: Event) => void; +} + +export interface AnalyticsContextValue { + pages: AnalyticsPage[]; + tracker: AnalyticsTracker; + merchantId: number; + ip: string; +} + +export type AnalyticsPageProps = { + /** Tag of this page. */ + tag: AnalyticsTag; + + /** Type of this page. */ + type: AnalyticsPageType; + + /** Children in the page. */ + children?: React.ReactNode; + + /** merchant id */ + merchantId?: number; + + /** user ip address */ + ip?: string; +}; + +export type AnalyticsComponentProps = { + children: (context: AnalyticsContextValue) => React.ReactNode; +}; + +export declare const ROOT_ANALYTICS_CONTEXT: AnalyticsContextValue; + +export enum AnalyticsEvents { + CLICK = 'click', + DROPDOWN_CHANGE = 'dropdown_change', + SWITCH_ON = 'switch_on', + SWITCH_OFF = 'switch_off', + DROPDOWN_SELECT = 'dropdown_select', + CHECKBOX_ON = 'checkbox_on', + CHECKBOX_OFF = 'checkbox_off', + RADIOBUTTON_SELECT = 'radiobutton_select', + DATEPICKER_CHANGE = 'datepicker_date_change', + DAYPICKER_CHANGE = 'daypicker_date_change', + KEYBOARD_EVENT = 'keyboard_event', +} + +export enum AnalyticsComponentType { + BUTTON = 'button', + DROPDOWN = 'dropdown', + SWITCH = 'switch', + PAGINATION = 'pagination', + ICON = 'icon', + LINK = 'link', + CHECKBOX = 'checkbox', + DATEPICKER = 'datepicker', + DAYPICKER = 'daypicker', + DATEPICKER_PRESET = 'datepicker_preset', + TAB = 'tab', + ACCORDION = 'accordion', + MODAL = 'modal', + INPUT = 'input', + TEXTAREA = 'textarea', + TAB_NAVIGATION = 'tab_navigation', +} + +export interface Event { + // temporarily mark them as strings + event: AnalyticsEvents | string; + type: AnalyticsComponentType | string; + tag: AnalyticsTag; + pages: AnalyticsPage[]; + merchantId?: number; + ip?: string; +} + +export type AnalyticsHandlerFn = (event: Record) => void; + +export declare const AnalyticsPage: FunctionComponent; + +export declare const AnalyticsComponent: FunctionComponent; +export interface ContainerProps extends AnalyticsProps { className?: string; columns?: number | string; gap?: string; @@ -34,7 +140,15 @@ export interface ContainerProps { alignContent?: string; } -export interface CellProps { +declare type TrackParams = Pick & + Partial>; + +export declare const useAnalytics: () => { + track: (trackParams: TrackParams) => void; + trackCallback: (trackParams: TrackParams) => () => void; +}; + +export interface CellProps extends AnalyticsProps { className?: string; width?: number; height?: number; @@ -52,7 +166,7 @@ interface IGrid { export declare const Grid: IGrid; -export interface CheckboxProps { +export interface CheckboxProps extends AnalyticsProps { label: string; id: string; name?: string; @@ -74,7 +188,7 @@ export interface MenuItem { smallSize?: boolean; } -export interface ModalProps { +export interface ModalProps extends AnalyticsProps { isModalOpen: boolean; closeModal: () => any; children: ReactNode; @@ -87,7 +201,7 @@ export declare const Modal: FunctionComponent; export type NoteType = 'informative' | 'success' | 'warning' | 'danger'; -export interface NoteProps { +export interface NoteProps extends AnalyticsProps { title: string; text: string; type: NoteType; @@ -95,7 +209,7 @@ export interface NoteProps { export declare const Note: FunctionComponent; -export interface RadioProps { +export interface RadioProps extends AnalyticsProps { label: string; id: string; name?: string; @@ -110,11 +224,17 @@ export interface RadioProps { export declare const Radio: FunctionComponent; +export interface EditableFields { + fn: (props: ColumnFunctionProps) => void; + validationSchema: ObjectSchema; +} + export interface TableColumn { title: string; key: string; render?: (props: TableColumn$RenderProps) => ReactNode; style?: CSSProperties; + editable?: EditableFields; } export interface TableRowData extends Object { @@ -126,6 +246,12 @@ export interface TableColumn$RenderProps { rowValues: T; } +export interface ColumnFunctionProps { + value: string; + currentValue: string; + id: number; +} + export interface RowAction { icon?: string; onClick?: (id: number | string) => any; @@ -147,7 +273,14 @@ export interface TableOptions { headerSection?: Node | JSX.Element | null; } -export interface TableProps { +interface TableButtonProps extends AnalyticsProps { + label: string; + icon?: string | ReactNode; + onClick: (e: SyntheticEvent) => any; + type: string; +} + +export interface TableProps extends AnalyticsProps { columns: Array>; data: Array; showLoader?: boolean; @@ -155,13 +288,11 @@ export interface TableProps { className?: string; style?: CSSProperties; options?: Partial>; - tableButton?: { - label: string; - icon?: string | Node | JSX.Element; - onClick: (e: SyntheticEvent) => any; - type: string; - }; + tableButton?: Array; actionsRowTitle?: string; + editableById?: string; + hasItemEntries?: boolean; + totalItems?: number; } interface TableState { @@ -182,7 +313,7 @@ export declare class Table extends Component< renderColumns: (data: Array>) => ReactNodeArray; renderRows: (data: Array) => ReactNodeArray; } -interface NavigationTab { +interface NavigationTab extends AnalyticsProps { title: string; } @@ -192,7 +323,10 @@ export interface TabContentProps { iconModifiers?: Array; } -export interface TabNavigationProps extends DivHTMLAttributes, TabContentProps { +export interface TabNavigationProps + extends DivHTMLAttributes, + TabContentProps, + AnalyticsProps { tabs: Array; onTabClick: (index: number) => any; selectedTabIndex: number; @@ -206,7 +340,7 @@ interface TabInfo { name: string; } -interface TabsProps { +interface TabsProps extends AnalyticsProps { tabs: Array; selectedTabIndex: number; onTabClick: (index: number) => void; @@ -220,7 +354,7 @@ export interface ActionButtonRenderProps { closeAccordion: (e?: SyntheticEvent<*>) => void; } -export interface AccordionPanel { +export interface AccordionPanel extends AnalyticsProps { label: string; icon?: ReactNode; iconTooltip?: TooltipProps; @@ -234,7 +368,7 @@ export interface AccordionProps { isExtendable?: boolean; width?: string; extendWidth?: string; - onActivePanelChange?: (index: number) => void; + onActivePanelChange?: (index: number, label: string) => void; shouldClose?: boolean; onRequestClose?: () => void; } @@ -269,7 +403,8 @@ export type ButtonModifier = export interface ButtonProps extends ButtonHTMLAttributes, - ButtonContentProps { + ButtonContentProps, + AnalyticsProps { buttonModifiers?: Array; size?: ButtonSize; style?: CSSProperties; @@ -281,7 +416,7 @@ export interface ButtonProps export declare class Button extends Component> {} -export interface CardProps { +export interface CardProps extends AnalyticsProps { title?: string; titleVariant?: string; className?: string; @@ -299,7 +434,7 @@ export type DayPicker$OnDateChange = (values: DayPicker$OnDateChange$Arguments) export type DayPicker$OnFocusChange = (focusedInput: boolean) => any; -export interface DayPickerProps { +export interface DayPickerProps extends AnalyticsProps { isOutsideRange?: (day: number) => any; onDateChange: (date: Moment) => any; onFocusChange: (focused: any) => any; @@ -312,6 +447,7 @@ export interface DayPickerProps { placeholder?: string; onClose?: () => any; disablePastDays?: boolean; + displayFormat?: string; } export declare const DayPicker: FunctionComponent; @@ -325,7 +461,7 @@ export type DatePicker$OnDateChange = (values: DatePicker$OnDateChange$Arguments export type DatePicker$OnFocusChange = (focusedInput: FocusedInputShape | null) => any; -export interface DatePickerProps { +export interface DatePickerProps extends AnalyticsProps { startDate: Moment; endDate?: Moment; startDateId?: string; @@ -342,6 +478,7 @@ export interface DatePickerProps { activePeriodPreset?: string; disabled?: boolean; showPresets?: boolean; + showPresetsWithDropdown?: boolean; showInnerPresets?: boolean; } @@ -374,12 +511,13 @@ export declare class DatePicker extends Component ReactNode; } -export interface Option { +export interface Option extends AnalyticsProps { value: string; displayName: string; + disabled?: boolean; } -export interface DefaultOption { +export interface DefaultOption extends AnalyticsProps { displayName: string; disabled?: boolean; } @@ -390,7 +528,7 @@ export type DropdownModifier = | 'fontSizeMedium' | 'fontSizeLarge'; -export interface DropdownProps extends AllHTMLAttributes { +export interface DropdownProps extends AllHTMLAttributes, AnalyticsProps { value: string; onChange?: (value: string) => any; color?: string; @@ -404,20 +542,28 @@ export interface DropdownProps extends AllHTMLAttributes { export declare const Dropdown: FunctionComponent; +export interface DrawerProps extends AnalyticsProps { + handleClose: () => void; + isOpen: boolean; + width?: string; +} + +export declare const Drawer: FunctionComponent; + export type LabelModifier = | 'fontSizeExtraSmall' | 'fontSizeSmall' | 'fontSizeMedium' | 'fontSizeLarge'; -export interface LabelProps { +export interface LabelProps extends AnalyticsProps { disabled?: boolean; modifiers?: Array; } export declare const Label: StyledComponent<'label', Theme, LabelProps>; -export interface LoaderProps { +export interface LoaderProps extends AnalyticsProps { height?: number; width?: number; color?: string; @@ -429,7 +575,7 @@ export declare const Loader: StyledComponent<'div', Theme, LoaderProps>; export type NotificationVariant = 'success' | 'danger' | 'warning'; -interface NotificationProps { +interface NotificationProps extends AnalyticsProps { title: string; content: string; variant?: NotificationVariant; @@ -447,7 +593,7 @@ interface INotification extends FunctionComponent { export declare const Notification: INotification; -export interface PaginationProps { +export interface PaginationProps extends AnalyticsProps { onPageChange: (pageNumber: number) => any; totalItems: number; startPage?: number; @@ -461,7 +607,7 @@ export type PillLabelModifier = 'primary' | 'info' | 'success' | 'danger' | 'war export type PillLabelSize = 'xs' | 'sm' | 'md' | 'lg'; -interface PillLabelProps { +interface PillLabelProps extends AnalyticsProps { modifiers?: Array; size?: PillLabelSize; } @@ -470,7 +616,7 @@ export declare const PillLabel: StyledComponent<'span', Theme, PillLabelProps>; export type ProgressType = 'circle' | 'line'; -export interface ProgressProps { +export interface ProgressProps extends AnalyticsProps { type?: ProgressType; strokeWidth?: number; strokeColor?: string; @@ -487,7 +633,7 @@ export interface ProgressProps { export declare const Progress: FunctionComponent; -export interface SwitchProps { +export interface SwitchProps extends AnalyticsProps { checked: boolean; disabled?: boolean; id: string; @@ -501,7 +647,7 @@ export declare const TextArea: StyledComponent<'input', Theme>; export type InputSize = 'xs' | 'sm' | 'md' | 'lg'; -export interface InputProps extends AllHTMLAttributes { +export interface InputProps extends AllHTMLAttributes, AnalyticsProps { ref?: Ref; size?: InputSize; style?: CSSProperties; @@ -527,7 +673,7 @@ export interface TypographyProps { export declare const Typography: FunctionComponent; -export interface Theme { +export interface Theme extends AnalyticsProps { palette: { primary: { main: string; @@ -629,7 +775,7 @@ interface IColors { emerald: '#2ecc71'; peterRiver: '#3498db'; amethyst: '#9b59b6'; - asphalt: '#282c35', + asphalt: '#282c35'; wetAsphalt: '#34495e'; greenSea: '#16a085'; nephritis: '#27ae60'; @@ -656,7 +802,7 @@ export type FadeEasing = 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-o export type TooltipBehavior = 'hover' | 'click' | 'ref'; -export interface TooltipProps { +export interface TooltipProps extends AnalyticsProps { behavior?: TooltipBehavior; durationOnClick?: number; arrowWidth?: number; @@ -691,13 +837,13 @@ export declare class Tooltip extends Component { type TransitionVariant = 'fadeInLeft' | 'fadeInRight' | 'fadeOutLeft' | 'fadeOutRight'; -interface Step { +export interface Step extends AnalyticsProps { isDisabled: boolean; isCompleted: boolean; component: ReactNode; } -interface StepWizardProps { +interface StepWizardProps extends AnalyticsProps { steps: Array; activeStep: number; className?: string; diff --git a/package.json b/package.json index 27b088cc3..f58632c83 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@inplayer-org/inplayer-ui", "sideEffects": false, - "version": "2.5.0", + "version": "2.5.28", "author": "InPlayer", "description": "InPlayer React UI Components", "main": "dist/inplayer-ui.cjs.js", @@ -38,17 +38,17 @@ "styled-components": "^5.2.1" }, "devDependencies": { - "@babel/cli": "^7.12.1", - "@babel/core": "^7.12.3", + "@babel/cli": "^7.12.10", + "@babel/core": "^7.12.10", "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-private-methods": "^7.12.1", "@babel/plugin-transform-classes": "^7.12.1", - "@babel/preset-env": "^7.12.1", - "@babel/preset-react": "^7.12.5", - "@babel/preset-typescript": "^7.12.1", - "@rollup/plugin-babel": "^5.2.1", + "@babel/preset-env": "^7.12.11", + "@babel/preset-react": "^7.12.10", + "@babel/preset-typescript": "^7.12.7", + "@rollup/plugin-babel": "^5.2.2", "@rollup/plugin-commonjs": "^16.0.0", - "@rollup/plugin-image": "^2.0.5", + "@rollup/plugin-image": "^2.0.6", "@rollup/plugin-node-resolve": "^10.0.0", "@storybook/addon-docs": "^6.0.21", "@storybook/addon-info": "^5.3.21", @@ -56,34 +56,34 @@ "@storybook/react": "^6.0.21", "@types/dompurify": "^2.0.4", "@types/lodash": "^4.14.165", - "@types/react": "^16.9.56", + "@types/react": "^17.0.0", "@types/react-dates": "^21.8.0", - "@types/react-dom": "^16.9.9", + "@types/react-dom": "^17.0.0", "@types/react-toggle": "^4.0.2", "@types/styled-components": "^5.1.4", "@typescript-eslint/eslint-plugin": "4.3.0", - "@typescript-eslint/parser": "4.8.2", + "@typescript-eslint/parser": "4.13.0", "babel-eslint": "^10.1.0", - "babel-loader": "^8.1.0", + "babel-loader": "^8.2.2", "babel-plugin-styled-components": "1.10.5", - "dompurify": "^2.0.17", - "eslint": "^7.9.0", - "eslint-config-airbnb": "^18.2.0", + "dompurify": "^2.2.6", + "eslint": "^7.17.0", + "eslint-config-airbnb": "^18.2.1", "eslint-config-prettier": "^6.11.0", "eslint-config-react-app": "^5.2.1", "eslint-loader": "^4.0.2", - "eslint-plugin-import": "^2.22.0", - "eslint-plugin-jsx-a11y": "^6.3.1", - "eslint-plugin-prettier": "^3.1.4", - "eslint-plugin-react": "^7.20.6", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-prettier": "^3.3.1", + "eslint-plugin-react": "^7.22.0", "eslint-plugin-react-hooks": "^4.1.2", "eslint-watch": "^7.0.0", - "html-react-parser": "^0.14.1", + "html-react-parser": "^0.14.3", "lodash-es": "^4.17.15", "moment": "^2.29.1", - "polished": "^4.0.3", - "prettier": "^2.1.2", - "rc-progress": "^3.1.0", + "polished": "^4.0.5", + "prettier": "^2.2.1", + "rc-progress": "^3.1.3", "react": "^17.0.1", "react-addons-shallow-compare": "^15.6.2", "react-dates": "^21.8.0", @@ -92,15 +92,19 @@ "react-paginate": "^6.5.0", "rimraf": "^3.0.2", "rollup-plugin-copy": "^3.3.0", - "rollup-plugin-filesize": "^9.0.2", + "rollup-plugin-filesize": "^9.1.0", "rollup-plugin-terser": "^7.0.2", "styled-components": "^5.2.1", "styled-components-modifiers": "^1.2.5", "styled-tools": "^1.7.2", - "stylelint": "^13.7.2", + "stylelint": "^13.8.0", "stylelint-config-standard": "^20.0.0", "stylelint-config-styled-components": "^0.1.1", "stylelint-processor-styled-components": "^1.10.0", - "typescript": "^4.0.5" + "typescript": "^4.1.3" + }, + "dependencies": { + "formik": "^2.1.5", + "yup": "^0.32.11" } -} +} \ No newline at end of file diff --git a/src/analytics/AnalyticsComponent.tsx b/src/analytics/AnalyticsComponent.tsx new file mode 100644 index 000000000..c436aa832 --- /dev/null +++ b/src/analytics/AnalyticsComponent.tsx @@ -0,0 +1,13 @@ +import React, { ReactNode } from 'react'; +import { AnalyticsContext, AnalyticsContextValue } from './AnalyticsPage'; + +export type AnalyticsComponentProps = { + children: (context: AnalyticsContextValue) => ReactNode; +}; + +/** A component to wrap other components with to gain access to the AnalyticsContextValue. */ +const AnalyticsComponent = ({ children }: AnalyticsComponentProps) => ( + {children} +); + +export default AnalyticsComponent; diff --git a/src/analytics/AnalyticsPage.tsx b/src/analytics/AnalyticsPage.tsx new file mode 100644 index 000000000..25ebfdf0a --- /dev/null +++ b/src/analytics/AnalyticsPage.tsx @@ -0,0 +1,74 @@ +import React, { createContext } from 'react'; +import AnalyticsTracker from './AnalyticsTracker'; + +export type AnalyticsTag = string; + +export enum AnalyticsPageType { + PAGE = 'page', + MODAL = 'modal', + TAB = 'tab', +} + +export interface AnalyticsPageObject { + tag: AnalyticsTag; + type: AnalyticsPageType; +} + +export interface AnalyticsContextValue { + pages: AnalyticsPageObject[]; + tracker: AnalyticsTracker; + merchantId: number; + ip: string; +} + +export const ROOT_ANALYTICS_CONTEXT: AnalyticsContextValue = { + pages: [], + tracker: new AnalyticsTracker(), + merchantId: 0, + ip: '', +}; + +export const AnalyticsContext = createContext(ROOT_ANALYTICS_CONTEXT); + +export interface AnalyticsPageProps { + /** Tag of this page. */ + tag: AnalyticsTag; + + /** Type of this page. */ + type: AnalyticsPageType; + + /** Children in the page. */ + children?: React.ReactNode; + + /** merchant id */ + merchantId?: number; + + /** user ip address */ + ip?: string; +} + +/** Registers a subpage in the analytics component hierarchy. */ +const AnalyticsPageComponent = ({ + tag, + type, + children, + merchantId: mid, + ip: ipAddress, +}: AnalyticsPageProps) => ( + + {({ pages, tracker, merchantId, ip }) => ( + + {children} + + )} + +); + +export default AnalyticsPageComponent; diff --git a/src/analytics/AnalyticsTracker.ts b/src/analytics/AnalyticsTracker.ts new file mode 100644 index 000000000..edf9fb6b2 --- /dev/null +++ b/src/analytics/AnalyticsTracker.ts @@ -0,0 +1,67 @@ +import { AnalyticsPageObject, AnalyticsTag } from './AnalyticsPage'; + +export enum AnalyticsEvents { + CLICK = 'click', + DROPDOWN_CHANGE = 'dropdown_change', + SWITCH_ON = 'switch_on', + SWITCH_OFF = 'switch_off', + DROPDOWN_SELECT = 'dropdown_select', + CHECKBOX_ON = 'checkbox_on', + CHECKBOX_OFF = 'checkbox_off', + RADIOBUTTON_SELECT = 'radiobutton_select', + DATEPICKER_CHANGE = 'datepicker_date_change', + DAYPICKER_CHANGE = 'daypicker_date_change', + KEYBOARD_EVENT = 'keyboard_event', + FOCUS_OUT = 'focus_out', +} + +export enum AnalyticsComponentType { + BUTTON = 'button', + DROPDOWN = 'dropdown', + SWITCH = 'switch', + PAGINATION = 'pagination', + ICON = 'icon', + LINK = 'link', + CHECKBOX = 'checkbox', + DATEPICKER = 'datepicker', + DAYPICKER = 'daypicker', + DATEPICKER_PRESET = 'datepicker_preset', + TAB = 'tab', + ACCORDION = 'accordion', + MODAL = 'modal', + INPUT = 'input', + TEXTAREA = 'textarea', + TAB_NAVIGATION = 'tab_navigation', + RADIO = 'radio', +} + +export interface TrackEvent { + event: AnalyticsEvents; + type: AnalyticsComponentType; + tag: AnalyticsTag; + pages: AnalyticsPageObject[]; + merchantId: number; + ip: string; +} + +export type AnalyticsHandlerFn = (event: TrackEvent) => void; + +export default class AnalyticsTracker { + handlers: AnalyticsHandlerFn[] = []; + + registerHandler = (fn: AnalyticsHandlerFn) => { + this.handlers.push(fn); + + return () => { + this.unregisterHandler(fn); + }; + }; + + unregisterHandler = (fn: AnalyticsHandlerFn) => { + this.handlers = this.handlers.filter((handler) => handler !== fn); + }; + + track = (event: TrackEvent) => { + this.handlers.forEach((handler) => handler(event)); + }; +} diff --git a/src/analytics/externalTypes.ts b/src/analytics/externalTypes.ts new file mode 100644 index 000000000..862c98a0d --- /dev/null +++ b/src/analytics/externalTypes.ts @@ -0,0 +1,18 @@ +import { AnalyticsTag } from './AnalyticsPage'; + +export interface AnalyticsProps { + /** Unique identifier of the component within the logical page hierarchy. */ + tag?: AnalyticsTag; + + /** Posiition of the component within a table-like component. */ + position?: { + /** Unique ID of the row (usually what you would use as a key in react). */ + id: string; + + /** Row index. */ + row?: number; + + /** Column index or name. */ + column?: number | string; + }; +} diff --git a/src/analytics/index.ts b/src/analytics/index.ts new file mode 100644 index 000000000..29171144c --- /dev/null +++ b/src/analytics/index.ts @@ -0,0 +1,26 @@ +export { + default as AnalyticsPage, + ROOT_ANALYTICS_CONTEXT, + AnalyticsContext, +} from './AnalyticsPage'; + +export { + default as AnalyticsTracker, + AnalyticsEvents, + AnalyticsComponentType, +} from './AnalyticsTracker'; + +export { default as AnalyticsComponent, AnalyticsComponentProps } from './AnalyticsComponent'; + +export { default as useAnalytics } from './useAnalytics'; + +export type { + AnalyticsTag, + AnalyticsContextValue, + AnalyticsPageProps, + AnalyticsPageType, +} from './AnalyticsPage'; + +export type { TrackEvent as Event, AnalyticsHandlerFn } from './AnalyticsTracker'; + +export type { AnalyticsProps } from './externalTypes'; diff --git a/src/analytics/useAnalytics.ts b/src/analytics/useAnalytics.ts new file mode 100644 index 000000000..a65f6a3c8 --- /dev/null +++ b/src/analytics/useAnalytics.ts @@ -0,0 +1,32 @@ +import { useContext, useCallback, useMemo } from 'react'; +import { TrackEvent } from './AnalyticsTracker'; +import { AnalyticsContext } from './AnalyticsPage'; + +type TrackParams = Pick & + Partial>; + +const useAnalytics = () => { + const { pages, tracker, merchantId, ip } = useContext(AnalyticsContext); + + const track = useCallback( + ({ event, type, tag, ...optional }: TrackParams) => { + if (!tag) { + return; + } + + tracker.track({ event, type, tag, pages, merchantId, ip, ...optional }); + }, + [pages, tracker, merchantId, ip] + ); + + const trackCallback = useCallback( + (trackParams: TrackParams) => () => { + track(trackParams); + }, + [] + ); + + return useMemo(() => ({ track, trackCallback }), [track, trackCallback]); +}; + +export default useAnalytics; diff --git a/src/components/Accordion/Accordion.tsx b/src/components/Accordion/Accordion.tsx index ac16b8015..69d9c6fbd 100644 --- a/src/components/Accordion/Accordion.tsx +++ b/src/components/Accordion/Accordion.tsx @@ -5,6 +5,7 @@ import Arrow from './Arrow'; import { TooltipProps } from '../Tooltip/Tooltip'; import { AccordionWrapper } from './styled'; import AccordionPanel from './AccordionPanel'; +import { AnalyticsProps } from '../../analytics'; // Types type Panel = { @@ -13,7 +14,7 @@ type Panel = { iconTooltip?: TooltipProps; renderContent: () => any; disabled?: boolean; -}; +} & AnalyticsProps; type Props = { panels: Array; @@ -21,7 +22,7 @@ type Props = { width?: string; extendWidth?: string; isExtendable?: boolean; - onActivePanelChange?: (index: number) => void; + onActivePanelChange?: (index: number, label: string) => void; shouldClose?: boolean; onRequestClose?: () => void; }; @@ -39,20 +40,20 @@ export const Accordion = ({ const [activePanel, setActivePanel] = useState(-1); const [open, setOpen] = useState(false); - const openPanel = (panelIndex: number) => { + const openPanel = (panelIndex: number, label: string) => { if (panelIndex !== activePanel) { setActivePanel(panelIndex); - if (onActivePanelChange) onActivePanelChange(panelIndex); + if (onActivePanelChange) onActivePanelChange(panelIndex, label); } }; const closePanel = (e?: SyntheticEvent) => { if (e) e.stopPropagation(); setActivePanel(-1); - if (onActivePanelChange) onActivePanelChange(-1); + if (onActivePanelChange) onActivePanelChange(-1, ''); }; - const togglePanel = (panelIndex: number) => (e: any) => { + const togglePanel = (panelIndex: number, label: string) => (e: any) => { e.stopPropagation(); if (activePanel !== -1) { if (shouldClose) { @@ -61,7 +62,7 @@ export const Accordion = ({ onRequestClose(); } } else { - openPanel(panelIndex); + openPanel(panelIndex, label); } }; @@ -78,10 +79,9 @@ export const Accordion = ({ >
{panels.map((panel, index) => { - const { icon, iconTooltip, label, renderContent, disabled = false } = panel; + const { icon, iconTooltip, label, renderContent, disabled = false, tag = '' } = panel; const isActive = activePanel === index; const isOtherPanelActive = !isActive && activePanel !== -1; - return (
); diff --git a/src/components/Accordion/AccordionPanel.tsx b/src/components/Accordion/AccordionPanel.tsx index e663d8538..69fcafcd8 100644 --- a/src/components/Accordion/AccordionPanel.tsx +++ b/src/components/Accordion/AccordionPanel.tsx @@ -1,9 +1,15 @@ import React, { SyntheticEvent, ReactElement } from 'react'; import styled, { css } from 'styled-components'; import { ifProp, ifNotProp, prop } from 'styled-tools'; +import { FaAngleUp, FaAngleDown } from 'react-icons/fa'; +import { + AnalyticsComponent, + AnalyticsProps, + AnalyticsEvents, + AnalyticsComponentType, +} from '../../analytics'; // Components -import { FaAngleUp, FaAngleDown } from 'react-icons/fa'; import colors from '../../theme/colors'; import Tooltip, { TooltipProps } from '../Tooltip/Tooltip'; import Typography from '../Typography'; @@ -18,7 +24,7 @@ type AccordionPanelHeaderProps = { isActive: boolean; disabled: boolean; onClick: any; -}; +} & AnalyticsProps; type AccordionTitleProps = { isActive: boolean; @@ -124,7 +130,7 @@ type Props = { renderContent: (actions: { closePanel: (e?: SyntheticEvent) => void }) => any; closePanel: (e?: SyntheticEvent) => void; disabled: boolean; -}; +} & AnalyticsProps; const AccordionPanel = ({ label, @@ -137,21 +143,42 @@ const AccordionPanel = ({ disabled, togglePanel, closePanel, + tag = '', }: Props) => ( <> - - - {label} - - - {!isOtherPanelActive && (iconTooltip ? {icon} : icon)} - {!disabled && (isActive ? : )} - - + + {({ pages, tracker, merchantId, ip }) => ( + ) => { + if (!disabled) { + togglePanel(e); + if (tag && !isActive) { + tracker.track({ + event: AnalyticsEvents.CLICK, + type: AnalyticsComponentType.ACCORDION, + tag, + pages, + merchantId, + ip, + }); + } + } + }} + isActive={isActive} + tag={tag} + > + + {label} + + + {!isOtherPanelActive && + (iconTooltip ? {icon} : icon)} + {!disabled && (isActive ? : )} + + + )} + {isActive && renderContent({ closePanel })} diff --git a/src/components/Accordion/panels.tsx b/src/components/Accordion/panels.tsx index 79449425c..40e32ff86 100644 --- a/src/components/Accordion/panels.tsx +++ b/src/components/Accordion/panels.tsx @@ -16,6 +16,7 @@ export const panels = [

Long Content for the accordion1

), + tag: 'tag1', }, { label: 'Disabled Accordion', @@ -31,6 +32,7 @@ export const panels = [ ), disabled: true, + tag: 'tag2', }, { label: 'Accordion3', @@ -45,5 +47,6 @@ export const panels = [

Long Content for the accordion3

), + tag: 'tag3', }, ]; diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index b258c5d0c..1a8388edf 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,6 +1,12 @@ import React, { ButtonHTMLAttributes, ReactElement } from 'react'; import styled, { css } from 'styled-components'; import ButtonWrapper from './ButtonWrapper'; +import { + AnalyticsProps, + AnalyticsComponent, + AnalyticsEvents, + AnalyticsComponentType, +} from '../../analytics'; export type Size = 'xs' | 'sm' | 'md' | 'lg'; @@ -17,7 +23,7 @@ export type Props = ButtonHTMLAttributes & className?: string; fullWidth?: boolean; fullHeight?: boolean; - }; + } & AnalyticsProps; const ContentHolder = styled.span` margin-top: 2px; @@ -54,6 +60,7 @@ const Content = ({ icon = null, iconPosition = 'left', children }: ContentProps) const Button = ({ size = 'md', + tag, buttonModifiers, className = '', icon, @@ -61,20 +68,39 @@ const Button = ({ children, fullWidth, fullHeight, + onClick, ...rest }: Props) => ( - - - {children} - - + + {({ pages, tracker, merchantId, ip }) => ( + { + onClick?.(e); + + if (tag) { + tracker.track({ + event: AnalyticsEvents.CLICK, + type: AnalyticsComponentType.BUTTON, + tag, + pages, + merchantId, + ip, + }); + } + }} + {...rest} + > + + {children} + + + )} + ); export default Button; diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx index b4cbfda1d..780784f51 100644 --- a/src/components/Card/Card.tsx +++ b/src/components/Card/Card.tsx @@ -4,6 +4,7 @@ import { transparentize } from 'polished'; import colors from '../../theme/colors'; import Typography, { TypographyVariant } from '../Typography/Typography'; import CardContent from './CardContent'; +import { AnalyticsProps } from '../../analytics'; const CardWrapper = styled.div` display: flex; @@ -32,12 +33,12 @@ const CardTitle = styled(Typography)` display: inline-block; `; -interface Props { +type Props = { title?: string; titleVariant?: TypographyVariant; className?: string; children: ReactChild; -} +} & AnalyticsProps; const Card = ({ title, titleVariant = 'h1', className = '', children }: Props) => ( diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index f5171b587..7d9199014 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -1,6 +1,12 @@ import React, { ChangeEvent, RefObject } from 'react'; import Label from '../Label'; import CheckboxWrapper from './CheckboxWrapper'; +import { + AnalyticsComponent, + AnalyticsComponentType, + AnalyticsProps, + AnalyticsEvents, +} from '../../analytics'; type Props = { label: string; @@ -10,7 +16,7 @@ type Props = { onChange: (checked: boolean) => any; containerRef?: RefObject | null; disabled?: boolean; -}; +} & AnalyticsProps; const Checkbox: React.FC = ({ label, @@ -18,15 +24,50 @@ const Checkbox: React.FC = ({ onChange, containerRef = null, disabled = false, + tag = '', ...rest }) => { const onCheckboxChange = (e: ChangeEvent): any => onChange(e.target.checked); return ( - - - - + + {({ pages, tracker, merchantId, ip }) => ( + + { + onCheckboxChange(e); + + if (tag) { + if (e.target.checked) { + tracker.track({ + event: AnalyticsEvents.CHECKBOX_ON, + type: AnalyticsComponentType.CHECKBOX, + tag, + pages, + merchantId, + ip, + }); + } else { + tracker.track({ + event: AnalyticsEvents.CHECKBOX_OFF, + type: AnalyticsComponentType.CHECKBOX, + tag, + pages, + merchantId, + ip, + }); + } + } + }} + {...rest} + /> + + + )} + ); }; diff --git a/src/components/Checkbox/CheckboxWrapper.ts b/src/components/Checkbox/CheckboxWrapper.ts index fd8cbf6d6..be2ac830a 100644 --- a/src/components/Checkbox/CheckboxWrapper.ts +++ b/src/components/Checkbox/CheckboxWrapper.ts @@ -10,7 +10,7 @@ const CheckboxWrapper = styled.div<{ theme: DefaultTheme }>` > input + label { position: relative; - padding-left: 1.5rem; + padding-left: 1.5em; cursor: pointer; &::before { @@ -18,8 +18,8 @@ const CheckboxWrapper = styled.div<{ theme: DefaultTheme }>` position: absolute; left: 0; top: 0; - width: 1rem; - height: 1rem; + width: 1em; + height: 1em; border: 1px solid ${colors.gray}; background: ${colors.white}; border-radius: 2px; @@ -44,8 +44,8 @@ const CheckboxWrapper = styled.div<{ theme: DefaultTheme }>` border: 2px solid ${colors.skyBlue}; border-top: none; border-right: none; - width: 0.5rem; - height: 0.25rem; + width: 0.5em; + height: 0.25em; opacity: 1; transform: scale(1) rotate(-45deg); } @@ -56,8 +56,8 @@ const CheckboxWrapper = styled.div<{ theme: DefaultTheme }>` border: 2px solid ${colors.white}; border-top: none; border-right: none; - width: 0.5rem; - height: 0.25rem; + width: 0.5em; + height: 0.25em; opacity: 1; transform: scale(0); } diff --git a/src/components/DatePicker/DatePicker.tsx b/src/components/DatePicker/DatePicker.tsx index fa1da1da4..52b5aedfc 100644 --- a/src/components/DatePicker/DatePicker.tsx +++ b/src/components/DatePicker/DatePicker.tsx @@ -1,21 +1,44 @@ import React, { useState, useEffect } from 'react'; import moment, { Moment } from 'moment'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { FocusedInputShape, DateRangePicker } from 'react-dates'; +import { FaCalendarAlt } from 'react-icons/fa'; +import { snakeCase } from 'lodash'; import colors from '../../theme/colors'; import { getMonthOptions, getYearOptions } from '../../utils/helpers'; import Dropdown from '../Dropdown'; +import { Option } from '../Dropdown/Dropdown'; import DatePickerWrapper from './DatePickerWrapper'; import { PERIODS, INNERPERIODS } from './periods'; import { Styled } from './styles'; +import { + AnalyticsComponent, + AnalyticsEvents, + AnalyticsComponentType, + AnalyticsProps, +} from '../../analytics'; import 'react-dates/initialize'; -const ContentHolder = styled.div` - display: flex; - justify-content: flex-end; - width: 86%; +const ContentHolder = styled.div<{ isDropdown?: boolean }>` margin: 0 auto; padding: 15px 0 15px; + ${({ isDropdown }) => + !isDropdown && + css` + display: flex; + justify-content: flex-end; + width: 86%; + `} +`; + +const StyledDropdown = styled(Dropdown)` + margin-right: 1rem; + color: ${colors.blue}; + background-color: transparent; + border: 1px solid ${colors.blue}; + font-size: ${({ theme }) => theme.font.sizes.extraSmall}; + font-weight: ${({ theme }) => theme.font.weights.bold}; + border-radius: 5px; `; const AnalyticsPeriods = styled.div` @@ -44,7 +67,7 @@ const StyledSpan = styled.span` } `; -interface DateChangeArgs { +export interface DateChangeArgs { startDate: Moment | null; endDate: Moment | null; } @@ -82,7 +105,8 @@ type Props = { showPresets?: boolean; showInnerPresets?: boolean; className?: string; -}; + showPresetsWithDropdown?: boolean; +} & AnalyticsProps; const DatePicker = ({ activePeriodPreset = '', @@ -99,6 +123,8 @@ const DatePicker = ({ focusedInput, className = '', minimumNights = 0, + showPresetsWithDropdown = false, + tag = '', }: Props) => { const [activePeriod, setActivePeriod] = useState(''); @@ -172,7 +198,7 @@ const DatePicker = ({ const renderDatePresets = () => { if (!showInnerPresets) return ''; - let presets = []; + let presets: string[] = []; if (displayPresets[0] === 'default') { presets = [ @@ -186,23 +212,38 @@ const DatePicker = ({ presets = [...displayPresets]; } return ( - - {presets.map((text: string) => ( - handleRangeClick(text as Period)} - > - {text} - - ))} - + + {({ pages, tracker, merchantId, ip }) => ( + + {presets.map((text: string) => ( + { + tracker.track({ + event: AnalyticsEvents.CLICK, + type: AnalyticsComponentType.DATEPICKER_PRESET, + tag: `datepicker_${snakeCase(text)}`, + pages, + merchantId, + ip, + }); + handleRangeClick(text as Period); + }} + > + {text} + + ))} + + )} + ); }; const renderMonthElement = ({ month, onMonthSelect, onYearSelect }: RenderMonthElementProps) => ( onMonthSelect(month, val)} @@ -210,6 +251,7 @@ const DatePicker = ({ onYearSelect(month, val)} @@ -220,37 +262,249 @@ const DatePicker = ({ const handleDateChange = ({ startDate, endDate }: DateChangeArgs) => { onDateChange({ startDate, endDate }); + setActivePeriod(''); + }; + + const options: Array