# dynamic-story

Latest version **0.2.3** (published 2019-05-25) · 0 weekly downloads

## Install

```sh
npm install dynamic-story
pnpm add dynamic-story
yarn add dynamic-story
bun add dynamic-story
```

## Health

**Score 20/100 (F)** — status: abandoned.

Positive: esm support; no vulnerabilities.

Warnings: low downloads; no types; pre 1.0.

Negative: abandoned; low maintenance score.

## Facts

| | |
|---|---|
| Version | 0.2.3 |
| Published | 2019-05-25 |
| First published | 2019-05-11 |
| Weekly downloads | 0 |
| TypeScript types | none |
| Module format | ESM + CommonJS |
| Dependencies | 8 |
| Unpacked size | 66.4 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | wasa42 |

## Links

- npm: https://www.npmjs.com/package/dynamic-story
- Repository: https://github.com/WaSa42/dynamic-story
- Homepage: https://WaSa42.github.io/dynamic-story
- Issues: https://github.com/WaSa42/dynamic-story/issues
- npm.io page: https://npm.io/package/dynamic-story

## Dependencies (8)

- [react](https://npm.io/package/react.md) ^16.8.6
- [redux](https://npm.io/package/redux.md) ^4.0.1
- [react-dom](https://npm.io/package/react-dom.md) ^16.8.6
- [classnames](https://npm.io/package/classnames.md) ^2.2.6
- [prop-types](https://npm.io/package/prop-types.md) ^15.7.2
- [react-redux](https://npm.io/package/react-redux.md) ^7.0.1
- [redux-thunk](https://npm.io/package/redux-thunk.md) ^2.3.0
- [react-children-addons](https://npm.io/package/react-children-addons.md) ^1.0.8

## Recent versions

- 0.2.3 (latest) — 2019-05-25
- 0.2.2 — 2019-05-25
- 0.2.1 — 2019-05-12
- 0.2.0 — 2019-05-12
- 0.1.2 — 2019-05-12
- 0.1.1 — 2019-05-11
- 0.1.0 — 2019-05-11

## README

<p align="center">
  <img src="src/assets/image/logo.png" alt="RDS logo">
</p>

# React Dynamic Story β
Beta version of a minimalist **[react](https://github.com/facebook/react)**
game framework for dynamic story telling.

```bash
npm install dynamic-story -S
```

## Get started

**RDS** works with [redux](https://github.com/reduxjs/redux).
First, we need to combined the module reducer with the main reducer:

```jsx
// './reducer.js'

import { combineReducers } from 'redux';
import { reducer as dynamicStory } from 'dynamic-story';

export default combineReducers({
  dynamicStory, // name is important, don't change
  // ...yourOtherReducers
});
```

Then we provide store to our new App as described in
[react-redux](https://github.com/reduxjs/react-redux)
documentation.

```jsx
// './index.js'

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import { applyMiddleware, compose, createStore } from 'redux';
import thunk from 'redux-thunk';

import reducer from './reducer';
// import MyStory from './components/MyStory';

const middlewares = [thunk];
const store = createStore(reducer, compose(applyMiddleware(...middlewares)));

const App = () => (
  <Provider store={store}>
    {/* <MyStory /> */}
  </Provider>
);

ReactDOM.render(<App />, document.getElementById('root'));
```

We are ready to create our `<Story />` component
and display our first story Element: a `<Card />`

```jsx
// './components/MyStory.js'

import React from 'react';
import { Story, Card } from 'dynamic-story';

const MyStory = () => (
  <Story id="my-story">
    <Card
      text="
        When Mr. and Mrs. Dursley woke up on the dull, gray Tuesday our story
        starts, there was nothing about the cloudy sky outside to suggest that
        strange and mysterious things would soon be happening all over the country.
      "
    />
  </Story>
);

export default MyStory;
```
Wow, that's pretty ugly, is it normal ?
Yes, there's no style in this module, but RDS works well with
[bootstrap](https://github.com/twbs/bootstrap) and
[animate.css](https://github.com/daneden/animate.css).
See how to [implements styles](#Styling).
See also the [Card Component](#Card <small>component</small>).

Then, we could `connect()` our Story component in order
to dispatch actions and go forward in the story.

```jsx
// './components/MyStory.js'

import React from 'react';
import { connect } from 'react-redux';
import { Story, Card, goForward, goTo } from 'dynamic-story';

const MyStory = ({ dispatch }) => (
  <Story id="my-story">
    <Card text="Text n°0" onTimeout={() => dispatch(goForward())} timeout={5000} />
    <Card text="Text n°1" onTimeout={() => dispatch(goTo(3))} timeout={2000} />
    <Card text="Text n°2" />
    <Card text="Text n°3" />
  </Story>
);

export default connect()(MyStory);
```

With this example, Card with text n°0 will be revealed first
and after a 5 seconds delay, the Card coming right after is going to be shown.
Then, after 2 seconds, history will go forward directly to text n°3 skipping n°2.

**Element's order is important !**
Indexes are used as unique ids in the history state.

### Fragments
Your story may required a lot of elements like `<Card />` and more.
To preserve understanding of your schema, you can divide parts of the story
into fragments and use them directly in `<Story > ...your fragments </ Story>`

```jsx
// './components/fragments/Intro.js'

import React from 'react';
import PropTypes from 'prop-types';
import readingTime from 'reading-time';
import { Card, goForward } from 'dynamic-story';

import image from '../assets/image/test.jpg';

const Fragment = ({ dispatch }) => (
  <>
    <Card
      isSkipAble
      onTimeout={() => dispatch(goForward())}
      text="Lorem Ipsum"
      timeout={({ text }) => readingTime(text).time}
    />
    <Card
      isSkipAble
      onTimeout={() => dispatch(goForward())}
      text="Dolor sit amet"
      timeout={({ text }) => readingTime(text).time}
    />
  </>
);

Fragment.propTypes = { dispatch: PropTypes.func.isRequired };

export default Fragment;
);
```

```jsx
// './components/MyStory.js'

import React from 'react';
import { connect } from 'react-redux';
import { Story, Card, actions } from 'dynamic-story';

import Intro from './components/fragments/Intro';

const MyStory = (props) => (
  <Story id="my-story">
    {Intro(props)}
    <Card text="Some text coming after intro" />
  </Story>
);


export default connect()(MyStory);
```

## API
### Story <small>component</small>
`<Story />` is the main component where you can load all your story elements.
```jsx
Story.propTypes = {
  className: PropTypes.string,
  children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]).isRequired,
  current: PropTypes.number.isRequired,
  darkMode: PropTypes.bool,
  history: PropTypes.arrayOf(PropTypes.object).isRequired,
};
```
```jsx
<Story id="myStory" className={classNames('styled', { 'bg-dark': context.isDark })}>
  {Intro(props)}
  <Card className="text-center" text="End" onClick={() => dispatch(resetStory())} />
</Story>
```

You can set Debug mode using `<Debug />` component instead of `<Story />`.

**Here the full demo example:**

```jsx
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { connect } from 'react-redux';

import {
  Card,
  Debug,
  DebugButton,
  ErrorBoundary,
  getSchema,
  GoBackward,
  Header,
  resetStory,
  setContext,
  Story,
} from 'dynamic-story';

import Intro from './fragments/Intro';
import Choice1 from './fragments/Choice1';

import initialContext from './context';
import banner from './assets/image/logo.png';

const Content = props => (
  <>
    <Header
      className="border-0 bg-transparent"
      banner={{ src: banner, alt: 'React Dynamic Story Banner' }}
    />
    {Intro(props)}
    {Choice1(props)}
    <Card
      className="text-center"
      text="End"
      onClick={() => props.dispatch(resetStory())}
      title="End"
      category="End"
      comment="This is End"
      tags={['end', 'game over']}
    />
  </>
);

const MyStory = (props) => {
  const { context, dispatch, settings } = props;
  const { darkMode, debug } = settings;
  const schema = getSchema(Content(props));

  React.useEffect(() => { if (!context) dispatch(setContext(initialContext)); }, [context]);

  return context && (
    <>
      <ErrorBoundary>
        {React.createElement(
          debug ? Debug : Story,
          { id: 'myStory', className: classNames('styled', { 'bg-darker': darkMode }) },
          Content({ ...props, schema }),
        )}
      </ErrorBoundary>
      {(!debug && process.env.NODE_ENV === 'development') && (
        <div className="fixed-top m-2 text-right">
          <GoBackward className={classNames('btn-sm mr-2', { 'btn-dark': darkMode, 'btn-light': !darkMode })} />
          <DebugButton className={classNames('btn-sm mr-2', { 'btn-dark': darkMode, 'btn-light': !darkMode })} />
        </div>
      )}
    </>
  );
};

MyStory.propTypes = {
  dispatch: PropTypes.func.isRequired,
  context: PropTypes.objectOf(PropTypes.any),
  settings: PropTypes.shape({ debug: PropTypes.bool }),
};

MyStory.defaultProps = {
  context: null,
  settings: {},
};

export default connect(state => ({ ...state.dynamicStory }))(MyStory);
```

### Card <small>component</small>
`<Card />` is story element template implementing useful props and behaviors.
```jsx
Card.propTypes = {
  animationEntrance: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
  children: PropTypes.oneOfType([
    PropTypes.array,
    PropTypes.element,
    PropTypes.number,
    PropTypes.string,
  ]),
  choices: PropTypes.objectOf(PropTypes.shape({
    className: PropTypes.string,
    text: PropTypes.string.isRequired,
    onClick: PropTypes.func.isRequired,
  })),
  className: PropTypes.string,
  darkMode: PropTypes.bool,
  debug: PropTypes.bool,
  disabled: PropTypes.bool,
  index: PropTypes.number,
  isSkipAble: PropTypes.bool,
  onClick: PropTypes.func,
  onKeyPress: PropTypes.func,
  onReveal: PropTypes.func,
  onTimeout: PropTypes.func,
  tabIndex: PropTypes.number,
  text: PropTypes.string,
  timeout: PropTypes.oneOfType([PropTypes.func, PropTypes.number]),
};

Card.defaultProps = {
  animationEntrance: 'animated fadeIn',
  children: undefined,
  choices: null,
  className: '',
  darkMode: false,
  debug: false,
  disabled: false,
  index: null,
  isSkipAble: false,
  onClick: null,
  onKeyPress: null,
  onReveal: null,
  onTimeout: null,
  tabIndex: null,
  text: undefined,
  timeout: 0,
};
```
```jsx
import React from 'react';
import PropTypes from 'prop-types';
import { Card, goForward, updateContext, updateSettings } from '../lib';

const Fragment = ({ dispatch, context }) => {
  const choices = {
    wait: {
      className: 'btn btn-link text-white',
      text: 'Wait',
      onClick: () => dispatch(goForward()),
    },
    turnOfTheLight: {
      className: 'btn btn-link text-white',
      text: 'Turn of the light',
      onClick: () => {
        dispatch(updateContext({ environment: { ...context.environment, isLightOn: false } }));
        dispatch(updateSettings({ darkMode: true }));
        dispatch(goForward());
      },
    },
  };

  return (
    <>
      <Card choices={choices} className="text-white bg-dark">
        <div className="card-body">
          <h5 className="card-title">Time to choose</h5>
          <p className="card-text">
            Which are the posibilities ?
            You can wait for someone to come
            or press the button and turn of the light.
          </p>
        </div>
      </Card>
    </>
  );
};

Fragment.propTypes = {
  dispatch: PropTypes.func.isRequired,
  context: PropTypes.objectOf(PropTypes.any).isRequired,
};

export default Fragment;
```

### Header <small>component</small>
`<Header />` is story element template extending `<Card />`.
```jsx
Header.propTypes = {
  banner: PropTypes.shape({
    src: PropTypes.string.isRequired,
    alt: PropTypes.string.isRequired,
  }),
  children: PropTypes.arrayOf(PropTypes.element),
  className: PropTypes.string,
};

Header.defaultProps = {
  banner: null,
  className: '',
  children: undefined,
};
```
```jsx
<Header banner={{ src: banner, alt: 'React Dynamic Story Banner' }} />
```
### setContext <small>action</small>
|Arg|Type|Example|
|-------|--------|--------------|
|**context**|object|`{ environment: { light: 'off' } }`|

Override context reducer.

### updateContext <small>action</small>
|Arg|Type|Example|
|-------|--------|--------------|
|**context**|object|`{ test: 'test' }`|

Merge with context reducer.

### setCurrent <small>action</small>
|Arg|Type|Example|
|-------|--------|--------------|
|**current**|number|`6`|

Override current reducer.

### setHistory <small>action</small>
|Arg|Type|Example|
|-------|--------|--------------|
|**history**|array|`[{ from: 0, to: 1, context: {} }]`|

Override history reducer.

### setSettings <small>action</small>
|Arg|Type|Example|
|-------|--------|--------------|
|**settings**|object|`{ darkMode: true }`|

Override settings reducer.

### updateSettings <small>action</small>
|Arg|Type|Example|
|-------|--------|--------------|
|**settings**|object|`{ darkMode: true }`|

Merge settings reducer.

### resetStory <small>action</small>
Reset all root (dynamicStory) reducer and delete storage if any
(work great with redux-persist);

### goTo <small>action</small>
|Arg|Type|Example|
|-------|--------|--------------|
|**from**|number|`0`|
|**to**|number|`1`|
|**context**|object|`{ light: 'off' }`|

Go from current element (from)
to a specific story element index (to)
and save current context.

### goBackwardTo <small>action</small>
|Arg|Type|Example|
|-------|--------|--------------|
|**index**|number|`1`|
|**context**|object|`undefined`|
|**to**|number|`4`|

Go backward to a specific **history** iteration `index`
or a story element in the history `to`
and reload context.

### goForward <small>action</small>
|Arg|Type|Example|
|-------|--------|--------------|
|**skip**|number|`0`|
|**context**|object|`undefined`|

Go forward in story elements order and skip certain elements.
Also save current context in history state.

### goBackward <small>action</small>
|Arg|Type|Example|
|-------|--------|--------------|
|**skip**|number|`0`|

Go backward in story elements order and skip certain elements.

## Styling

RDS works well with
[bootstrap](https://github.com/twbs/bootstrap) and
[animate.css](https://github.com/daneden/animate.css).

```bash
npm install node-sass bootstrap animate.css -S
```
If your're in a [create-react-app](https://github.com/facebook/create-react-app)
project, there are no configuration needed, otherwise, you might add a sass-loader
to your bundle manager (Webpack).

Create a bootstrap Sass file and import only what's needed:
```scss
// './assets/style/bootstrap.scss'
$theme-colors: (
  "black": #000,
);

// Required
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";

// Optional
@import "~bootstrap/scss/badge";
@import "~bootstrap/scss/buttons";
@import "~bootstrap/scss/card";
@import "~bootstrap/scss/grid";
@import "~bootstrap/scss/nav";
@import "~bootstrap/scss/reboot";
@import "~bootstrap/scss/type";
@import "~bootstrap/scss/utilities";
```
Same with animate.css:
```scss
// './assets/style/animate.scss'

// Required
@import "~animate.css/source/_base.css";

// Optional
@import "~animate.css/source/fading_entrances/fadeIn.css";
@import "~animate.css/source/bouncing_entrances/bounceIn.css";
```
Then import Sass files in your JS, Webpack will do the rest:
```jsx
// './index.js'

import React from 'react';
// ... Imports

import './assets/style/bootstrap.scss';
import './assets/style/animate.scss';
import './index.scss';

```

## Troubleshooting
First, make sure to install all dependencies if `npm` has failed doing it:
- [classnames](https://github.com/JedWatson/classnames)
- [prop-types](https://github.com/facebook/prop-types)
- [react](https://github.com/facebook/react)
- [react-children-addons](https://github.com/birkir/react-children-addons)
- [react-dom](https://github.com/facebook/react-dom)
- [react-redux](https://github.com/reduxjs/react-redux)
- [redux](https://github.com/reduxjs/redux)
- [redux-thunk](https://github.com/reduxjs/redux-thunk)

```bash
npm install classnames classnamesprop-types react react-children-addons react-dom react-redux redux redux-thunk -S
```

## TODO
- [X] Accessibility and focus
- [X] `<Header />` template component
- [X] Mode debug for `<Story />`
- [X] Persit/Save demo
- [ ] Quick Time Event plugin
- [ ] X Box Controller support

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

---
_Source: https://npm.io/package/dynamic-story · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
