diff --git a/packages/abtest/README.md b/packages/abtest/README.md new file mode 100644 index 0000000000..dc1520814e --- /dev/null +++ b/packages/abtest/README.md @@ -0,0 +1,66 @@ +# ndla-abtest + +WIP + +## Installation + +```sh +$ yarn add --save ndla-abtest +``` + +```sh +$ npm i --save ndla-abtest +``` + +## Usage + +```js +import { ExperimentsContext, Experiment, Variant } from '@ndla/abtest'; + +// clean experiments returned from Experiments service +const cleanExperiments = [ + { + id: '6bklbienTOuNQs9JwMrvog', // Experiment 1 ID + variant: { + // Use variant with index: 0 + index: 0, + name: '', + }, + }, + { + id: 'gKKvagBlQ5SyhxWP4TqK0g', // Experiment 2 ID + variant: { + // Use variant with index: 2 + index: 2, + name: '', + }, + }, +]; + +const experimentId = 'gKKvagBlQ5SyhxWP4TqK0g'; + + + +

Testing button title in app

+ + {({ experiments }) => ( + { + console.log('render details', variantData); + }}> + + Test 1 + + Test 2 + Test 3 + + )} + +
+
; +``` diff --git a/packages/abtest/package.json b/packages/abtest/package.json new file mode 100644 index 0000000000..0aec1dfdaa --- /dev/null +++ b/packages/abtest/package.json @@ -0,0 +1,25 @@ +{ + "name": "@ndla/abtest", + "version": "0.0.1", + "description": "AB-test Context", + "license": "GPL-3.0", + "main": "lib/index.js", + "module": "es/index.js", + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/NDLANO/frontend-packages.git/ndla-abtest/" + }, + "keywords": [ + "ndla", + "AB-test" + ], + "author": "ndla@knowit.no", + "files": [ + "lib", + "es" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/packages/abtest/src/Context.ts b/packages/abtest/src/Context.ts new file mode 100644 index 0000000000..eaa72f8d7b --- /dev/null +++ b/packages/abtest/src/Context.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2019-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React from 'react'; + +export const ExperimentsContext = React.createContext({}); diff --git a/packages/abtest/src/Experiment.tsx b/packages/abtest/src/Experiment.tsx new file mode 100644 index 0000000000..d2bb192b6b --- /dev/null +++ b/packages/abtest/src/Experiment.tsx @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2019-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React from 'react'; + +import { ExperimentsContext } from './Context'; + +export interface VariationsShape { + index?: number; + name?: string; + weight?: number; +} + +export interface ExperimentShape { + id: string; + variant: VariationsShape; +} + +interface Props { + experiments: ExperimentShape[]; + id: string; + onVariantMount?: void; + children: React.ReactNode[]; +} + +export const Experiment: React.FC = ({ + experiments, + id: experimentId, + onVariantMount, + children, +}) => { + const { Provider } = ExperimentsContext; + const useVariant = experiments.find( + experiment => + experiment.id.localeCompare(experimentId, undefined, { + sensitivity: 'base', + }) === 0, + ); + return ( + + {children} + + ); +}; + +interface fetchVariantIndexShape { + id: string; + experiments: ExperimentShape[]; +} + +export const fetchVariantIndex = ({ + experiments, + id: experimentId, +}: fetchVariantIndexShape) => { + const useVariant = experiments.find( + experiment => + experiment.id.localeCompare(experimentId, undefined, { + sensitivity: 'base', + }) === 0, + ); + return useVariant ? useVariant.variant : {}; +}; + +export const isValidExperiment = ({ + experiments, + id, +}: fetchVariantIndexShape) => { + return experiments && experiments.find(ex => ex.id === id); +}; diff --git a/packages/abtest/src/Variant.tsx b/packages/abtest/src/Variant.tsx new file mode 100644 index 0000000000..443a5cb9dc --- /dev/null +++ b/packages/abtest/src/Variant.tsx @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2019-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React from 'react'; +import { ExperimentsContext } from './Context'; +import { VariationsShape } from './Experiment'; + +interface Props { + variantIndex: number; + original?: boolean; + onVariantMount?: void; + children: React.ReactNode[]; +} + +interface ValueShape { + experimentId?: string; + variant?: VariationsShape; +} + +export class Variant extends React.Component { + componentDidMount() { + const { experimentId, variant, onVariantMount } = this.context; + const isActiveExperiment = this.isActive(this.context); + if (isActiveExperiment && onVariantMount) { + onVariantMount({ + expId: experimentId, + expVar: variant.index, + isActiveExperiment, + }); + } + } + isActive(value: ValueShape) { + const { variantIndex, original } = this.props; + return ( + (!value.variant && original) || + (value.variant && value.variant.index === variantIndex) + ); + } + render() { + const { Consumer } = ExperimentsContext; + return ( + + {(value: ValueShape) => + this.isActive(value) ? this.props.children : null + } + + ); + } +} + +Variant.contextType = ExperimentsContext; diff --git a/packages/abtest/src/cleanupExperiments.ts b/packages/abtest/src/cleanupExperiments.ts new file mode 100644 index 0000000000..2ea344a782 --- /dev/null +++ b/packages/abtest/src/cleanupExperiments.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2019-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { ExperimentShape, VariationsShape } from './Experiment'; + +export interface ExperimentShapeClean { + id: string; + variations: VariationsShape[]; +}; + +export function cleanupExperiments(experiments: ExperimentShapeClean[], cookieExperiments: ExperimentShape[]) { + return experiments.map(experiment => { + const { + id, + variations, + } = experiment; + + if (cookieExperiments) { + const experimentInCookie = cookieExperiments.find((cookieExperiments: ExperimentShape) => cookieExperiments.id === id); + if (experimentInCookie) { + return experimentInCookie; + } + } + + const pickVariant = Math.random(); + let variationsWeightCounter = 0; + const variationsTotal = variations.length - 1; + const winner = variations.find((variation: VariationsShape, index: number) => { + if (variationsWeightCounter + (variation.weight || 0) > pickVariant || index === variationsTotal) { + return true; + } else { + variationsWeightCounter += (variation.weight || 0); + return false; + } + }); + + if (typeof winner === 'object') { + winner.index = variations.findIndex(variant => variant.name === winner.name); + return { + id, + variant: winner, + } + } + return null; + }).filter(experiment => experiment); +}; \ No newline at end of file diff --git a/packages/abtest/src/index.ts b/packages/abtest/src/index.ts new file mode 100644 index 0000000000..b17c373527 --- /dev/null +++ b/packages/abtest/src/index.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2019-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { ExperimentsContext } from './Context'; +export { Experiment, fetchVariantIndex, isValidExperiment } from './Experiment'; +export { Variant } from './Variant'; +export { cleanupExperiments } from './cleanupExperiments'; diff --git a/packages/abtest/tsconfig.build.json b/packages/abtest/tsconfig.build.json new file mode 100644 index 0000000000..bdb104388c --- /dev/null +++ b/packages/abtest/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "baseUrl": "./", + "declarationDir": "./lib", + "rootDir": "./src" + }, + "include": ["./src"] +} diff --git a/packages/designmanual/stories/LanguageWrapper/LanguageWrapper.jsx b/packages/designmanual/stories/LanguageWrapper/LanguageWrapper.jsx index c768983e8a..a4a5e1b7f7 100644 --- a/packages/designmanual/stories/LanguageWrapper/LanguageWrapper.jsx +++ b/packages/designmanual/stories/LanguageWrapper/LanguageWrapper.jsx @@ -16,6 +16,7 @@ const messages = { nn: formatNestedMessages(messagesNN), en: formatNestedMessages(messagesEN), }; + export const LanguageContext = React.createContext(); class LanguageWrapperProvider extends Component { diff --git a/packages/ndla-icons/src/common/Hamburger.js b/packages/ndla-icons/src/common/Hamburger.js new file mode 100644 index 0000000000..ebaa12b546 --- /dev/null +++ b/packages/ndla-icons/src/common/Hamburger.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2019-present, NDLA. + * + * This source code is licensed under the GPLv3 license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +// N.B! AUTOGENERATED FILE. DO NOT EDIT +import React from 'react'; +import Icon from '../Icon'; + +const Hamburger = props => ( + + + + + +); + +export default Hamburger; diff --git a/packages/ndla-icons/src/common/index.js b/packages/ndla-icons/src/common/index.js index cf2a674ed9..367e2a3bf8 100644 --- a/packages/ndla-icons/src/common/index.js +++ b/packages/ndla-icons/src/common/index.js @@ -28,6 +28,7 @@ export { default as FileDownloadOutline } from './FileDownloadOutline'; export { default as Forward } from './Forward'; export { default as Fullscreen } from './Fullscreen'; export { default as Grid } from './Grid'; +export { default as Hamburger } from './Hamburger'; export { default as HelpCircle } from './HelpCircle'; export { default as HelpCircleDual } from './HelpCircleDual'; export { default as Home } from './Home'; diff --git a/packages/ndla-icons/svg/common/Hamburger.svg b/packages/ndla-icons/svg/common/Hamburger.svg new file mode 100644 index 0000000000..c50de1d5f8 --- /dev/null +++ b/packages/ndla-icons/svg/common/Hamburger.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ndla-modal/src/Modal.js b/packages/ndla-modal/src/Modal.js index 0b4e10376a..2595884afe 100644 --- a/packages/ndla-modal/src/Modal.js +++ b/packages/ndla-modal/src/Modal.js @@ -454,6 +454,7 @@ class Modal extends React.Component { this.containerRef = React.createRef(); this.scrollPosition = null; this.el = null; + this.wasOpen = false; this.uuid = uuid(); } @@ -489,8 +490,9 @@ class Modal extends React.Component { }, this.removedModal, ); - } else if (this.state.animateIn && this.state.isOpen) { + } else if (this.state.animateIn && this.state.isOpen && !this.wasOpen) { this.el = document.body.querySelector(`[data-modal='${this.uuid}']`); + this.wasOpen = true; if (this.props.onOpen) { this.props.onOpen(); } @@ -531,6 +533,7 @@ class Modal extends React.Component { removedModal() { this.scrollPosition = 0; + this.wasOpen = false; if (uuidList.indexOf(this.uuid) !== -1) { noScroll(false, this.uuid); uuidList.splice(uuidList.indexOf(this.uuid), 1); diff --git a/packages/ndla-ui/src/TopicMenu/TopicMenuButton.jsx b/packages/ndla-ui/src/TopicMenu/TopicMenuButton.jsx index 62c633662a..fe8efffffe 100644 --- a/packages/ndla-ui/src/TopicMenu/TopicMenuButton.jsx +++ b/packages/ndla-ui/src/TopicMenu/TopicMenuButton.jsx @@ -40,15 +40,20 @@ const style = css` } `; -const TopicMenuButton = ({ ndlaFilm, children, ...rest }) => ( +const TopicMenuButton = ({ ndlaFilm, children, Icon, ...rest }) => ( ); TopicMenuButton.propTypes = { children: PropTypes.node.isRequired, ndlaFilm: PropTypes.bool, + Icon: PropTypes.node, +}; + +TopicMenuButton.defaultProps = { + Icon: , }; export default TopicMenuButton; diff --git a/packages/ndla-ui/src/locale/messages-en.js b/packages/ndla-ui/src/locale/messages-en.js index 72887131e0..710cc79f0f 100644 --- a/packages/ndla-ui/src/locale/messages-en.js +++ b/packages/ndla-ui/src/locale/messages-en.js @@ -479,6 +479,15 @@ const messages = { newGroupTitle: 'What shall we call the new movie group?', }, }, + abTests: { + masthead: { + menu: { + topics: 'Topics', + overview: 'Overview', + subjectOverview: 'Subject overview', + }, + }, + }, }; export default messages; diff --git a/packages/ndla-ui/src/locale/messages-nb.js b/packages/ndla-ui/src/locale/messages-nb.js index 09af1a670e..4741b244d5 100644 --- a/packages/ndla-ui/src/locale/messages-nb.js +++ b/packages/ndla-ui/src/locale/messages-nb.js @@ -494,6 +494,15 @@ const messages = { newGroupTitle: 'Hva skal gruppen hete?', }, }, + abTests: { + masthead: { + menu: { + topics: 'Emner', + overview: 'Oversikt', + subjectOverview: 'Fagoversikt', + }, + }, + }, }; export default messages; diff --git a/packages/ndla-ui/src/locale/messages-nn.js b/packages/ndla-ui/src/locale/messages-nn.js index 95a4a6c295..7f0391766f 100644 --- a/packages/ndla-ui/src/locale/messages-nn.js +++ b/packages/ndla-ui/src/locale/messages-nn.js @@ -478,6 +478,15 @@ const messages = { newGroupTitle: 'Kva skal gruppen hete?', }, }, + abTests: { + masthead: { + menu: { + topics: 'Emner', + overview: 'Oversikt', + subjectOverview: 'Fagoversikt', + }, + }, + }, }; export default messages;