npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dynamic-story

v0.2.3

Published

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

Readme

React Dynamic Story β

Beta version of a minimalist react game framework for dynamic story telling.

npm install dynamic-story -S

Get started

RDS works with redux. First, we need to combined the module reducer with the main reducer:

// './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 documentation.

// './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 />

// './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 and animate.css. See how to implements styles. See also the [Card Component](#Card component).

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

// './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>

// './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;
);
// './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 component

<Story /> is the main component where you can load all your story elements.

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,
};
<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:

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 component

<Card /> is story element template implementing useful props and behaviors.

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,
};
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 component

<Header /> is story element template extending <Card />.

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,
};
<Header banner={{ src: banner, alt: 'React Dynamic Story Banner' }} />

setContext action

|Arg|Type|Example| |-------|--------|--------------| |context|object|{ environment: { light: 'off' } }|

Override context reducer.

updateContext action

|Arg|Type|Example| |-------|--------|--------------| |context|object|{ test: 'test' }|

Merge with context reducer.

setCurrent action

|Arg|Type|Example| |-------|--------|--------------| |current|number|6|

Override current reducer.

setHistory action

|Arg|Type|Example| |-------|--------|--------------| |history|array|[{ from: 0, to: 1, context: {} }]|

Override history reducer.

setSettings action

|Arg|Type|Example| |-------|--------|--------------| |settings|object|{ darkMode: true }|

Override settings reducer.

updateSettings action

|Arg|Type|Example| |-------|--------|--------------| |settings|object|{ darkMode: true }|

Merge settings reducer.

resetStory action

Reset all root (dynamicStory) reducer and delete storage if any (work great with redux-persist);

goTo action

|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 action

|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 action

|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 action

|Arg|Type|Example| |-------|--------|--------------| |skip|number|0|

Go backward in story elements order and skip certain elements.

Styling

RDS works well with bootstrap and animate.css.

npm install node-sass bootstrap animate.css -S

If your're in a 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:

// './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:

// './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:

// './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:

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 file for details.