Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions packages/abtest/README.md
Original file line number Diff line number Diff line change
@@ -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';

<ExperimentsContext.Provider
value={{
experiments: cleanExperiments,
}}>
<App>
<h1>Testing button title in app</h1>
<ExperimentsContext.Consumer>
{({ experiments }) => (
<Experiment
id={experimentId}
experiments={experiments}
onVariantMount={variantData => {
console.log('render details', variantData);
}}>
<Variant variantIndex={0} original>
Test 1
</Variant>
<Variant variantIndex={1}>Test 2</Variant>
<Variant variantIndex={2}>Test 3</Variant>
</Experiment>
)}
</ExperimentsContext.Consumer>
</App>
</ExperimentsContext.Provider>;
```
25 changes: 25 additions & 0 deletions packages/abtest/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
11 changes: 11 additions & 0 deletions packages/abtest/src/Context.ts
Original file line number Diff line number Diff line change
@@ -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({});
79 changes: 79 additions & 0 deletions packages/abtest/src/Experiment.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({
experiments,
id: experimentId,
onVariantMount,
children,
}) => {
const { Provider } = ExperimentsContext;
const useVariant = experiments.find(
experiment =>
experiment.id.localeCompare(experimentId, undefined, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hvorfor bruke localeCompare? Og hvorfor ikke bruke isValidExperiment funksjonen fra under?

sensitivity: 'base',
}) === 0,
);
return (
<Provider
value={{
variant: useVariant ? useVariant.variant : {},
experimentId,
onVariantMount,
}}>
{children}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bedre API og sende inn en array med varianter i stedet for renderprops? Trenger man ikke eksponere til ndla-frontend, og man trenger ikke context for man kan bare sende props direkte inn.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Takk for gode tilbakemeldinger. Vi ser på saken 👍

</Provider>
);
};

interface fetchVariantIndexShape {
id: string;
experiments: ExperimentShape[];
}

export const fetchVariantIndex = ({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kan ikke se at denne blir brukt noe sted? Viss den blir det bør den i en util funksjon

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);
};
56 changes: 56 additions & 0 deletions packages/abtest/src/Variant.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> {
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 (
<Consumer>
{(value: ValueShape) =>
this.isActive(value) ? this.props.children : null
}
</Consumer>
);
}
}

Variant.contextType = ExperimentsContext;
51 changes: 51 additions & 0 deletions packages/abtest/src/cleanupExperiments.ts
Original file line number Diff line number Diff line change
@@ -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);
};
12 changes: 12 additions & 0 deletions packages/abtest/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
9 changes: 9 additions & 0 deletions packages/abtest/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"baseUrl": "./",
"declarationDir": "./lib",
"rootDir": "./src"
},
"include": ["./src"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const messages = {
nn: formatNestedMessages(messagesNN),
en: formatNestedMessages(messagesEN),
};

export const LanguageContext = React.createContext();

class LanguageWrapperProvider extends Component {
Expand Down
26 changes: 26 additions & 0 deletions packages/ndla-icons/src/common/Hamburger.js
Original file line number Diff line number Diff line change
@@ -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 => (
<Icon
title="Hamburger"
viewBox="0 0 24 24"
data-license="Apache License 2.0"
data-source="Material Design"
{...props}>
<g>
<path d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" />
</g>
</Icon>
);

export default Hamburger;
1 change: 1 addition & 0 deletions packages/ndla-icons/src/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions packages/ndla-icons/svg/common/Hamburger.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion packages/ndla-modal/src/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
Expand Down
Loading