├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── custom.md
│ └── feature_request.md
├── .gitignore
├── .huskyrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── jest.config.js
├── lint-staged.config.js
├── package.json
├── packages
├── documentation
│ ├── .gitignore
│ ├── README.md
│ ├── babel.config.js
│ ├── docs
│ │ ├── api.md
│ │ ├── intro.md
│ │ └── tutorial-basics
│ │ │ ├── Header-stories.mdx
│ │ │ ├── _category_.json
│ │ │ ├── component-stories.mdx
│ │ │ ├── image-stories.mdx
│ │ │ ├── image-video-stories.mdx
│ │ │ ├── playground.mdx
│ │ │ ├── see-more.mdx
│ │ │ └── video-stories.mdx
│ ├── docusaurus.config.js
│ ├── package.json
│ ├── sidebars.js
│ ├── src
│ │ ├── components
│ │ │ ├── ComponentStories.js
│ │ │ ├── HeaderStories.js
│ │ │ ├── HomepageFeatures.js
│ │ │ ├── HomepageFeatures.module.css
│ │ │ ├── ImageStories.js
│ │ │ ├── ImageVideoStories.js
│ │ │ ├── SeeMoreStories.js
│ │ │ ├── VideoStories.js
│ │ │ └── styles.css
│ │ ├── css
│ │ │ └── custom.css
│ │ ├── pages
│ │ │ ├── index.js
│ │ │ ├── index.module.css
│ │ │ └── markdown-page.md
│ │ └── theme
│ │ │ └── ReactLiveScope
│ │ │ └── index.js
│ └── static
│ │ ├── .nojekyll
│ │ └── img
│ │ ├── 1.jpg
│ │ ├── 2.jpg
│ │ ├── 3.jpg
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon.ico
│ │ ├── logo.jpg
│ │ ├── logo.svg_
│ │ ├── site.webmanifest
│ │ └── tutorial
│ │ ├── docsVersionDropdown.png
│ │ └── localeDropdown.png
└── stories
│ ├── README.md
│ ├── package.json
│ ├── rollup.config.js
│ ├── src
│ ├── Components
│ │ ├── Actions
│ │ │ ├── Actions.component.tsx
│ │ │ ├── Actions.constants.ts
│ │ │ ├── Actions.styles.css
│ │ │ └── index.ts
│ │ ├── CustomComponent
│ │ │ ├── CustomComponents.component.tsx
│ │ │ ├── CustomComponents.styles.css
│ │ │ └── index.ts
│ │ ├── Image
│ │ │ ├── Image.component.tsx
│ │ │ ├── Image.styles.css
│ │ │ └── index.ts
│ │ ├── Progress
│ │ │ ├── Progress.component.tsx
│ │ │ ├── index.ts
│ │ │ └── progress.styles.css
│ │ ├── ProgressBar
│ │ │ ├── ProgressBar.component.tsx
│ │ │ ├── ProgressBar.styles.css
│ │ │ └── index.ts
│ │ ├── SeeMore
│ │ │ ├── SeeMore.component.tsx
│ │ │ ├── SeeMore.styles.css
│ │ │ └── index.ts
│ │ ├── SeeMoreComponent
│ │ │ ├── SeeMoreComponent.component.tsx
│ │ │ ├── SeeMoreComponent.styles.css
│ │ │ └── index.ts
│ │ ├── SoundIcon
│ │ │ ├── SoundIcon.component.tsx
│ │ │ └── index.ts
│ │ ├── Story
│ │ │ ├── Story.component.tsx
│ │ │ ├── Story.constants.ts
│ │ │ ├── Story.styles.css
│ │ │ └── index.ts
│ │ ├── Video
│ │ │ ├── Video.component.tsx
│ │ │ ├── Video.styles.css
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── Contexts
│ │ └── index.ts
│ ├── Hooks
│ │ └── index.ts
│ ├── declerations.d.ts
│ ├── index.tsx
│ ├── styles.css
│ ├── types.d.ts
│ └── utilities.ts
│ └── tsconfig.json
├── prettier.config.js
├── readme.md
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | end_of_line = lf
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [.md]
16 | insert_final_newline = false
17 | trim_trailing_whitespace = false
18 |
19 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/dist/**
2 | **/node_modules/**
3 |
4 | /node_modules
5 | /dist
6 | /lib
7 | /deploy
8 | /public/
9 |
10 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: [
4 | 'eslint:recommended',
5 | 'plugin:jest/recommended',
6 | 'prettier',
7 | 'plugin:@typescript-eslint/recommended',
8 | ],
9 | plugins: ['@typescript-eslint', 'react', 'import'],
10 | rules: {
11 | indent: ['error', 2],
12 | 'linebreak-style': 1,
13 | },
14 | parser: '@typescript-eslint/parser',
15 | parserOptions: {
16 | ecmaVersion: 2021,
17 | sourceType: 'module',
18 | ecmaFeatures: {
19 | jsx: true,
20 | useJSXTextNode: true,
21 | },
22 |
23 | ignorePatterns: [
24 | 'dist/**/*', // Ignore built files.
25 | ],
26 | },
27 | env: {
28 | es6: true,
29 | browser: true,
30 | node: true,
31 | },
32 | settings: {
33 | react: {
34 | version: 'detect',
35 | },
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/custom.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Custom issue template
3 | about: Describe this issue template's purpose here.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | logs/
3 | pids/
4 | lib-cov/
5 | coverage/
6 | jspm_packages/
7 | build/Release/
8 | node_modules/
9 | bower_components/
10 | coverage/
11 | dist/
12 | lib/
13 | local/
14 | reports/
15 | Thumbs.db
16 | dll/
17 | ~/
18 |
19 | *.log
20 | npm-debug.log*
21 | *.pid
22 | *.seed
23 | .nyc_output
24 | .lock-wscript
25 | .npm
26 | .history
27 | .node_repl_history
28 | .cache-loader/
29 | .eslintcache
30 | *.env
31 | .env
32 | .idea
33 | .vscode
34 | *.sublime-project
35 | *.sublime-workspaceg
36 | .DS_Store
37 | *.tgz
38 | *.zip
39 | .cache
40 | .history
41 | *.aes
42 | package-lock.json
43 | *.docz/**
44 | tags
45 | !tags/
46 | tags.lock
47 | tags.temp
48 |
49 |
--------------------------------------------------------------------------------
/.huskyrc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/.huskyrc
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | .
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # add contributing guidelines
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 hannad rehman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/jest.config.js
--------------------------------------------------------------------------------
/lint-staged.config.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/lint-staged.config.js
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "workspaces": [
4 | "packages/*"
5 | ],
6 | "version": "1.1.2",
7 | "description": "instagram style stories in react js",
8 | "scripts": {
9 | "start:docs": "cd ./packages/documentation/ && yarn start",
10 | "build:stories": "cd ./packages/stories/ && yarn build:watch",
11 | "build:docs": "cd ./packages/documentation/ && yarn build && yarn mv",
12 | "test": "yarn test",
13 | "lint": "eslint . --ext .ts",
14 | "install": "yarn install --frozen-lockfile"
15 | },
16 | "keywords": [
17 | "reactjs",
18 | "typescript",
19 | "instagram",
20 | "stories"
21 | ],
22 | "repository": "https://github.com/hannadrehman/stories-react/",
23 | "homepage": "https://hannadrehman.github.io/stories-react/",
24 | "bugs": "https://github.com/hannadrehman/stories-react/issues",
25 | "author": {
26 | "name": "Hannad Rehman",
27 | "email": "hannad63@gmail.com",
28 | "url": "https://hannadrehman.com"
29 | },
30 | "license": "MIT",
31 | "devDependencies": {
32 | "@typescript-eslint/eslint-plugin": "^5.54.0",
33 | "@typescript-eslint/parser": "^5.54.0",
34 | "eslint": "^8.35.0",
35 | "eslint-config-prettier": "^8.6.0",
36 | "eslint-plugin-import": "^2.27.5",
37 | "eslint-plugin-jest": "^27.2.1",
38 | "eslint-plugin-prettier": "^4.2.1",
39 | "eslint-plugin-react": "^7.32.2",
40 | "jest": "^29.4.3",
41 | "prettier": "^2.8.4",
42 | "typescript": "^4.9.5"
43 | },
44 | "dependencies": {
45 | "caniuse-lite": "^1.0.30001460"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/packages/documentation/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | /build
5 | # Generated files
6 | .docusaurus
7 | .cache-loader
8 |
9 | # Misc
10 | .DS_Store
11 | .env.local
12 | .env.development.local
13 | .env.test.local
14 | .env.production.local
15 |
16 | npm-debug.log*
17 | yarn-debug.log*
18 | yarn-error.log*
19 |
--------------------------------------------------------------------------------
/packages/documentation/README.md:
--------------------------------------------------------------------------------
1 | # Website
2 |
3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
4 |
5 | ### Installation
6 |
7 | ```
8 | $ yarn
9 | ```
10 |
11 | ### Local Development
12 |
13 | ```
14 | $ yarn start
15 | ```
16 |
17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
18 |
19 | ### Build
20 |
21 | ```
22 | $ yarn build
23 | ```
24 |
25 | This command generates static content into the `build` directory and can be served using any static contents hosting service.
26 |
27 | ### Deployment
28 |
29 | Using SSH:
30 |
31 | ```
32 | $ USE_SSH=true yarn deploy
33 | ```
34 |
35 | Not using SSH:
36 |
37 | ```
38 | $ GIT_USER= yarn deploy
39 | ```
40 |
41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
42 |
--------------------------------------------------------------------------------
/packages/documentation/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/documentation/docs/api.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | ---
4 |
5 | # Api
6 |
7 |
8 | ## Props
9 |
10 | | Property | Type | Default | Description |
11 | | ------------------------------| ------------------------------| --------------| --------------------------------------------------------------------------------------- |
12 | | `stories` | `IStoryObject[]` | `[]` | An array of story objects. description of `IStoryObject` is mentioned below |
13 | | `height` | `string` | `100%` | Height of story container |
14 | | `width` | `string` | `100%` | Width of story container |
15 | | `loop` | `boolean` | `false` | The last story loop to the first one and restart the stories |
16 | | `onStoryChange` | `function(index:number)` | `-` | Callback called when story changes |
17 | | `onStoriesStart` | `function` | `-` | Callback called when first story is rendered. it get called only once, |
18 | | `onAllStoriesEnd` | `function` | `-` | Callback called when last story gets completed. it will get called only once |
19 | | `currentIndex` | `number` | `-` | Current index of the story which should be selected first |
20 | | `defaultDuration` | `number` | `10000` | default duration in ms of stories if duration is not provided in the `IStoryObject` |
21 | | `classNames` | `IStoryClassNames` | `{}` | classnames to overide different sections of a story renderer |
22 | | `pauseStoryWhenInActiveWindow`| `boolean` | `true` | pauses story when window goes out of focus (user changes tab/minimizes browser etc |
23 |
24 | ## IStoryObject
25 |
26 | | Property | Type | Default | Description |
27 | | --------------------- |--------------------------------------| -------------| ------------------------------------------------------------|
28 | | `type` | `image , video , component` | `-` | type of story to render |
29 | | `url` | `string` | `-` | url of a story (image & video only) |
30 | | `duration` | `duration` | `10000` | duration in ms of the story |
31 | | `component` | `React Component` | `-` | custom component to render as a story |
32 | | `header` | `React Component` | `-` | header of a story |
33 | | `seeMore` | `string , boolean , React Component` | `true` | See more action text |
34 | | `seeMoreComponent` | `React Component` | `-` | see more component opens after clicking see more |
35 | | `onSeeMoreClick` | `function(index:number)` | `-` | Callback called when story see more is clicked |
36 |
37 |
38 | ## IStoryClassNames
39 |
40 | | Property | Type | Default | Description |
41 | | --------------------- |--------------------------------------| -------------| ------------------------------------------------------------|
42 | | `main` | `string` | `-` | classname for main container |
43 | | `progressContainer` | `string` | `-` | classname for progress line container |
44 | | `progressBarContainer`| `string` | `-` | classname for single progress bar box container |
45 | | `progressBar` | `string` | `-` | classname for progress bar |
46 | | `storyContainer` | `string` | `-` | classname for story container |
47 |
48 | ## Custom Component Story Props
49 |
50 |
51 | | Property | Type | | Description |
52 | | --------------------- |--------------------------------------| -------------| ------------------------------------------------------------|
53 | | `pause` | `function` | | call it to pause a story |
54 | | `resume` | `function` | | call it to resume a story |
55 | | `story` | `IStoryObject` | | current story properties |
56 | | `isPaused` | `boolean` | | state of a story |
57 |
--------------------------------------------------------------------------------
/packages/documentation/docs/intro.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # Intro
6 |
7 | Let's discover **React Instagram Stories** in less than **5 minutes**.
8 |
9 | ## Getting Started
10 |
11 |
12 | ## Install
13 |
14 |
15 | ```shell
16 | npm install stories-react
17 | ```
18 |
19 | ## Usage
20 |
21 |
22 | ```jsx
23 |
24 | import Stories from 'stories-react';
25 |
26 | const storySource = [
27 | {
28 | type: "image",
29 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
30 | duration: 5000
31 | },
32 | {
33 | type: "video",
34 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
35 | duration: 5000
36 | }
37 | ]
38 |
39 | function Profile(){
40 | return (
41 |
42 |
43 |
44 | )
45 | }
46 |
47 | ```
48 |
--------------------------------------------------------------------------------
/packages/documentation/docs/tutorial-basics/Header-stories.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 6
3 | ---
4 |
5 | # Stories with Header
6 |
7 | import HeaderStories from '../../src/components/HeaderStories';
8 |
9 |
10 |
11 |
12 | ```jsx
13 |
14 | import React from 'react';
15 | import Stories from 'stories-react';
16 | import 'stories-react/dist/index.css';
17 |
18 | function Head(props) {
19 | return (
20 |
30 |
31 |
41 |
42 |
50 |
59 | {props.name}
60 |
61 |
70 | {`${props.time} hours ago`}
71 |
72 |
73 |
74 | );
75 | }
76 |
77 | export default function HeaderStories() {
78 | const stories = [
79 | {
80 | type: 'image',
81 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
82 | duration: 15000,
83 | header: (
84 |
89 | ),
90 | },
91 | {
92 | type: 'image',
93 | duration: 6000,
94 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
95 | header: (
96 |
101 | ),
102 | },
103 | {
104 | duration: 7000,
105 | type: 'image,
106 | url: 'https://images.pexels.com/photos/10964888/pexels-photo-10964888.jpeg?w=300',
107 | header: (
108 |
113 | ),
114 | },
115 | ];
116 |
117 | return (
118 |
126 |
127 |
128 | );
129 | }
130 | '
131 | ```
132 |
--------------------------------------------------------------------------------
/packages/documentation/docs/tutorial-basics/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "position": 1,
3 | "label": "Basic Usage",
4 | "collapsible": false
5 | }
6 |
--------------------------------------------------------------------------------
/packages/documentation/docs/tutorial-basics/component-stories.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 4
3 | ---
4 |
5 | # Component Stories
6 |
7 | import ComponentStories from '../../src/components/ComponentStories';
8 |
9 |
10 |
11 |
12 | ```jsx
13 |
14 | import React from 'react';
15 | import Stories from 'stories-react';
16 | import 'stories-react/dist/index.css';
17 |
18 | function Copy() {
19 | return (
20 |
21 | Jaguar shark! So tell me - does it really exist? Checkmate... Eventually,
22 | you do plan to have dinosaurs on your dinosaur tour, right? Yeah, but your
23 | scientists were so preoccupied with whether or not they could, they didnt
24 | stop to think if they should.
25 |
26 | );
27 | }
28 |
29 | function HelpText() {
30 | return (
31 |
40 |
Hannad Rehman says:
41 |
42 |
43 | );
44 | }
45 |
46 | function ComponentWithInteractions() {
47 | return (
48 |
57 |
Component with interactions
58 |
61 |
62 |
63 |
64 | You need to add zindex >= 2
to any interaction u want in
65 | the component
66 |
67 |
69 | window.open('https://www.pexels.com/@imadclicks', '_blank')
70 | }
71 | style={{
72 | color: '#3399FF',
73 | border: '1px solid',
74 | borderColor: '#3399FF',
75 | borderRadius: '3px',
76 | height: '30px',
77 | cursor: 'pointer',
78 | position: 'relative',
79 | zIndex: '2',
80 | width: '100%',
81 | }}
82 | >
83 | Follow Imad Clicks on pexels for amazing pictures
84 |
85 |
86 | );
87 | }
88 |
89 | function ComponentApi(props) {
90 | return (
91 |
100 |
Component Api
101 |
102 |
103 |
A story can be paused and resumed too !
104 |
105 |
props.pause()}
107 | style={{
108 | color: '#3399FF',
109 | border: '1px solid',
110 | borderColor: '#3399FF',
111 | borderRadius: '3px',
112 | height: '30px',
113 | cursor: 'pointer',
114 | position: 'relative',
115 | zIndex: '2',
116 | width: '100%',
117 | }}
118 | >
119 | Pause Story
120 |
121 |
122 |
123 |
124 |
props.resume()}
126 | style={{
127 | color: 'rgb(255, 51, 108)',
128 | border: '1px solid',
129 | borderColor: 'rgb(255, 51, 108)',
130 | borderRadius: '3px',
131 | height: '30px',
132 | cursor: 'pointer',
133 | position: 'relative',
134 | zIndex: '2',
135 | width: '100%',
136 | }}
137 | >
138 | Resume Story
139 |
140 |
Make Sure button zIndex is >= 2
141 |
142 |
143 | {JSON.stringify(props, null, 2)}
144 |
145 |
Story object passed in props
146 |
147 | );
148 | }
149 |
150 | export default function ComponentStories() {
151 | const stories = [
152 | {
153 | type: 'component',
154 | duration: 9000,
155 | component: HelpText,
156 | },
157 | {
158 | type: 'component',
159 | duration: 30000,
160 | component: ComponentWithInteractions,
161 | },
162 | {
163 | duration: 9000,
164 | type: 'component',
165 | component: ComponentApi,
166 | },
167 | ];
168 |
169 | return (
170 |
178 |
179 |
180 | );
181 | }
182 |
183 | ```
184 |
--------------------------------------------------------------------------------
/packages/documentation/docs/tutorial-basics/image-stories.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # Image Stories
6 |
7 | import ImageStories from '../../src/components/ImageStories';
8 |
9 |
10 |
11 |
12 | ```jsx
13 |
14 | import React from 'react';
15 | import Stories from 'stories-react';
16 | import 'stories-react/dist/index.css';
17 |
18 | export default function ImagesStories() {
19 | const stories = [
20 | {
21 | type: 'image',
22 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
23 | duration: 5000,
24 | },
25 | {
26 | type: 'image',
27 | duration: 6000,
28 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
29 | },
30 | {
31 | duration: 7000,
32 | type: 'image',
33 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
34 | },
35 | ];
36 |
37 | return (
38 |
43 | );
44 | }
45 |
46 | ```
47 |
--------------------------------------------------------------------------------
/packages/documentation/docs/tutorial-basics/image-video-stories.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | ---
4 |
5 | # Image & Video Stories
6 |
7 | import ImageVideoStories from '../../src/components/ImageVideoStories';
8 |
9 |
10 |
11 |
12 | ```jsx
13 |
14 | import React from 'react';
15 | import Stories from 'stories-react';
16 | import 'stories-react/dist/index.css';
17 |
18 | export default function ImageVideoStories() {
19 | const stories = [
20 | {
21 | type: 'image',
22 | duration: 6000,
23 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
24 | },
25 | {
26 | type: 'video',
27 | url: 'https://assets.mixkit.co/videos/preview/mixkit-man-dancing-under-changing-lights-1240-large.mp4',
28 | duration: 28000,
29 | },
30 | {
31 | type: 'image',
32 | duration: 6000,
33 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
34 | },
35 | {
36 | type: 'video',
37 | url: 'https://assets.mixkit.co/videos/preview/mixkit-mother-with-her-little-daughter-eating-a-marshmallow-in-nature-39764-large.mp4',
38 | duration: 10000,
39 | },
40 | {
41 | type: 'video',
42 | url: 'https://assets.mixkit.co/videos/preview/mixkit-family-walking-together-in-nature-39767-large.mp4',
43 | duration: 10000,
44 | },
45 |
46 | {
47 | type: 'video',
48 | duration: 6000,
49 | url: 'https://assets.mixkit.co/videos/preview/mixkit-girl-in-neon-sign-1232-large.mp4',
50 | },
51 | {
52 | duration: 7000,
53 | type: 'image',
54 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
55 | },
56 | ];
57 | return (
58 |
66 |
67 |
68 | );
69 | }
70 |
71 |
72 | ```
73 |
--------------------------------------------------------------------------------
/packages/documentation/docs/tutorial-basics/playground.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 7
3 | ---
4 |
5 | # Playground
6 |
7 |
8 | ```jsx live
9 | function ImagesStories() {
10 | const stories = [
11 | {
12 | type: 'image',
13 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
14 | duration: 5000,
15 | },
16 | {
17 | type: 'image',
18 | duration: 6000,
19 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
20 | },
21 | {
22 | duration: 7000,
23 | type: 'image',
24 | url: 'https://images.pexels.com/photos/10964888/pexels-photo-10964888.jpeg?w=300',
25 | },
26 | ];
27 |
28 | return (
29 |
40 |
41 |
Change the code below and it will reflect in real time
42 |
43 | );
44 | }
45 | ```
46 |
--------------------------------------------------------------------------------
/packages/documentation/docs/tutorial-basics/see-more.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 5
3 | ---
4 |
5 | # Stories with See More
6 |
7 | import SeeMoreStories from '../../src/components/SeeMoreStories';
8 |
9 |
10 |
11 |
12 | ```jsx
13 |
14 | import React from 'react';
15 | import Stories from 'stories-react';
16 | import 'stories-react/dist/index.css';
17 |
18 | function SeeMoreComponent() {
19 | return (
20 |
29 |
Component opened from see more
30 |
33 |
34 |
35 |
36 | You need to add zindex >= 2
to any interaction u want in
37 | the component
38 |
39 |
41 | window.open('https://www.pexels.com/@imadclicks', '_blank')
42 | }
43 | style={{
44 | color: '#3399FF',
45 | border: '1px solid',
46 | borderColor: '#3399FF',
47 | borderRadius: '3px',
48 | height: '30px',
49 | cursor: 'pointer',
50 | position: 'relative',
51 | zIndex: '2',
52 | width: '100%',
53 | }}
54 | >
55 | Follow Imad Clicks on pexels for amazing pictures
56 |
57 |
58 | );
59 | }
60 |
61 | export default function ImagesStories() {
62 | const stories = [
63 | {
64 | type: 'image',
65 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
66 | duration: 5000,
67 | seeMore: true,
68 | seeMoreComponent: SeeMoreComponent,
69 | },
70 | {
71 | type: 'image',
72 | duration: 6000,
73 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
74 | seeMore: 'Custom See More String',
75 | seeMoreComponent: SeeMoreComponent,
76 | },
77 | {
78 | duration: 7000,
79 | type: 'image',
80 | url: 'https://images.pexels.com/photos/10964888/pexels-photo-10964888.jpeg?w=300',
81 | seeMore: (
82 |
83 | custom see more button
84 |
85 | ),
86 | seeMoreComponent: SeeMoreComponent,
87 | },
88 | ];
89 |
90 | return (
91 |
99 |
100 |
101 | );
102 | }
103 |
104 | ```
105 |
106 |
--------------------------------------------------------------------------------
/packages/documentation/docs/tutorial-basics/video-stories.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Video Stories
6 |
7 | import VideoStories from '../../src/components/VideoStories';
8 |
9 |
10 |
11 |
12 | ```jsx
13 |
14 | import React from 'react';
15 | import Stories from 'stories-react';
16 | import 'stories-react/dist/index.css';
17 |
18 | export default function VideoStories() {
19 | const stories = [
20 | {
21 | type: 'video',
22 | url: 'https://assets.mixkit.co/videos/preview/mixkit-man-dancing-under-changing-lights-1240-large.mp4',
23 | duration: 28000,
24 | },
25 | {
26 | type: 'video',
27 | url: 'https://assets.mixkit.co/videos/preview/mixkit-mother-with-her-little-daughter-eating-a-marshmallow-in-nature-39764-large.mp4',
28 | duration: 10000,
29 | },
30 | {
31 | type: 'video',
32 | url: 'https://assets.mixkit.co/videos/preview/mixkit-family-walking-together-in-nature-39767-large.mp4',
33 | duration: 10000,
34 | },
35 | {
36 | type: 'video',
37 | duration: 6000,
38 | url: 'https://assets.mixkit.co/videos/preview/mixkit-girl-in-neon-sign-1232-large.mp4',
39 | },
40 | {
41 | duration: 30000,
42 | type: 'video',
43 | url: 'https://assets.mixkit.co/videos/preview/mixkit-tree-with-yellow-flowers-1173-large.mp4',
44 | },
45 | ];
46 |
47 | return (
48 |
56 |
57 |
58 | );
59 | }
60 | ```
61 |
--------------------------------------------------------------------------------
/packages/documentation/docusaurus.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Note: type annotations allow type checking and IDEs autocompletion
3 |
4 | const lightCodeTheme = require('prism-react-renderer/themes/github');
5 | const darkCodeTheme = require('prism-react-renderer/themes/dracula');
6 |
7 | /** @type {import('@docusaurus/types').Config} */
8 | const config = {
9 | title: 'React Stories',
10 | tagline: 'Add instagram like stories to your web project',
11 | url: 'https://hannadrehman.github.io/',
12 | baseUrl: '/stories-react/',
13 | onBrokenLinks: 'throw',
14 | onBrokenMarkdownLinks: 'warn',
15 | favicon: 'img/favicon.ico',
16 | organizationName: 'hannadrehman', // Usually your GitHub org/user name.
17 | projectName: 'stories-react', // Usually your repo name.
18 | deploymentBranch: 'gh-pages',
19 | plugins: [],
20 | presets: [
21 | [
22 | 'classic',
23 | /** @type {import('@docusaurus/preset-classic').Options} */
24 | {
25 | docs: {
26 | sidebarPath: require.resolve('./sidebars.js'),
27 | // Please change this to your repo.
28 | editUrl:
29 | 'https://github.com/hannadrehman/stories-react/tree/main/packages/documentation/',
30 | },
31 | theme: {
32 | customCss: [
33 | require.resolve('./src/css/custom.css'),
34 | require.resolve('../stories/dist/index.css'),
35 | ],
36 | },
37 | gtag: {
38 | trackingID: 'G-C9LG1XG1P1',
39 | anonymizeIP: true,
40 | },
41 | },
42 | ],
43 | ],
44 | themes: ['@docusaurus/theme-live-codeblock'],
45 | themeConfig:
46 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */
47 | {
48 | colorMode: {
49 | defaultMode: 'light',
50 | disableSwitch: false,
51 | respectPrefersColorScheme: false,
52 | },
53 | navbar: {
54 | title: 'React Stories',
55 | logo: {
56 | alt: 'React Stories',
57 | src: 'img/logo.jpg',
58 | },
59 | items: [
60 | {
61 | type: 'doc',
62 | docId: 'intro',
63 | position: 'left',
64 | label: 'Docs',
65 | },
66 | {
67 | href: 'https://hannadrehman.com?ref=react-stories',
68 | label: 'Blog',
69 | position: 'left',
70 | 'aria-label': 'Hannad rehman Blog',
71 | },
72 |
73 | {
74 | href: 'https://github.com/hannadrehman/stories-react',
75 | label: 'GitHub',
76 | position: 'right',
77 | 'aria-label': 'GitHub repository',
78 | },
79 | ],
80 | },
81 | footer: {
82 | style: 'dark',
83 | links: [
84 | {
85 | title: 'Docs',
86 | items: [
87 | {
88 | label: 'Tutorial',
89 | to: '/docs/intro',
90 | },
91 | ],
92 | },
93 | {
94 | title: 'Community',
95 | items: [],
96 | },
97 | {
98 | title: 'More',
99 | items: [
100 | {
101 | label: 'GitHub',
102 | href: 'https://github.com/hannadrehman/stories-react',
103 | },
104 | ],
105 | },
106 | ],
107 | copyright: `Copyright © ${new Date().getFullYear()} React Stories. Built with Docusaurus.`,
108 | },
109 | prism: {
110 | theme: lightCodeTheme,
111 | darkTheme: darkCodeTheme,
112 | },
113 | liveCodeBlock: {
114 | playgroundPosition: 'top',
115 | },
116 | },
117 | };
118 |
119 | module.exports = config;
120 |
--------------------------------------------------------------------------------
/packages/documentation/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "documentation",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids",
15 | "mv": "rm -rf ../../docs && cp -R ./build ../../docs && rm -rf ./build"
16 | },
17 | "dependencies": {
18 | "@docusaurus/core": "2.3.1",
19 | "@docusaurus/plugin-google-gtag": "^2.3.1",
20 | "@docusaurus/preset-classic": "2.3.1",
21 | "@docusaurus/theme-live-codeblock": "^2.3.1",
22 | "@mdx-js/react": "^1.6.22",
23 | "clsx": "^1.2.1",
24 | "prism-react-renderer": "^1.3.5",
25 | "react": "^18.2.0",
26 | "react-dom": "^18.2.0"
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.5%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packages/documentation/sidebars.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creating a sidebar enables you to:
3 | - create an ordered group of docs
4 | - render a sidebar for each doc of that group
5 | - provide next/previous navigation
6 |
7 | The sidebars can be generated from the filesystem, or explicitly defined here.
8 |
9 | Create as many sidebars as you want.
10 | */
11 |
12 | // @ts-check
13 |
14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
15 | const sidebars = {
16 | // By default, Docusaurus generates a sidebar from the docs folder structure
17 | tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }],
18 |
19 | // But you can create a sidebar manually
20 | /*
21 | tutorialSidebar: [
22 | {
23 | type: 'category',
24 | label: 'Tutorial',
25 | items: ['hello'],
26 | },
27 | ],
28 | */
29 | };
30 |
31 | module.exports = sidebars;
32 |
--------------------------------------------------------------------------------
/packages/documentation/src/components/ComponentStories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Stories from 'stories-react';
3 | import 'stories-react/dist/index.css';
4 |
5 | function Copy() {
6 | return (
7 |
8 | Jaguar shark! So tell me - does it really exist? Checkmate... Eventually,
9 | you do plan to have dinosaurs on your dinosaur tour, right? Yeah, but your
10 | scientists were so preoccupied with whether or not they could, they didnt
11 | stop to think if they should.
12 |
13 | );
14 | }
15 |
16 | function HelpText() {
17 | return (
18 |
27 |
Hannad Rehman says:
28 |
29 |
30 | );
31 | }
32 |
33 | function ComponentWithInteractions() {
34 | return (
35 |
44 |
Component with interactions
45 |
48 |
49 |
50 |
51 | You need to add zindex >= 2
to any interaction u want in
52 | the component
53 |
54 |
56 | window.open('https://www.pexels.com/@imadclicks', '_blank')
57 | }
58 | style={{
59 | color: '#3399FF',
60 | border: '1px solid',
61 | borderColor: '#3399FF',
62 | borderRadius: '3px',
63 | height: '30px',
64 | cursor: 'pointer',
65 | position: 'relative',
66 | zIndex: '2',
67 | width: '100%',
68 | }}
69 | >
70 | Follow Imad Clicks on pexels for amazing pictures
71 |
72 |
73 | );
74 | }
75 |
76 | function ComponentApi(props) {
77 | return (
78 |
87 |
Component Api
88 |
89 |
90 |
A story can be paused and resumed too !
91 |
92 |
props.pause()}
94 | style={{
95 | color: '#3399FF',
96 | border: '1px solid',
97 | borderColor: '#3399FF',
98 | borderRadius: '3px',
99 | height: '30px',
100 | cursor: 'pointer',
101 | position: 'relative',
102 | zIndex: '2',
103 | width: '100%',
104 | }}
105 | >
106 | Pause Story
107 |
108 |
109 |
110 |
111 |
props.resume()}
113 | style={{
114 | color: 'rgb(255, 51, 108)',
115 | border: '1px solid',
116 | borderColor: 'rgb(255, 51, 108)',
117 | borderRadius: '3px',
118 | height: '30px',
119 | cursor: 'pointer',
120 | position: 'relative',
121 | zIndex: '2',
122 | width: '100%',
123 | }}
124 | >
125 | Resume Story
126 |
127 |
Make Sure button zIndex is >= 2
128 |
135 | {JSON.stringify(props, null, 4)}
136 |
137 |
Story object passed in props
138 |
139 | );
140 | }
141 |
142 | export default function ComponentStories() {
143 | const stories = [
144 | {
145 | type: 'component',
146 | duration: 9000,
147 | component: HelpText,
148 | },
149 | {
150 | type: 'component',
151 | duration: 30000,
152 | component: ComponentWithInteractions,
153 | },
154 | {
155 | duration: 9000,
156 | type: 'component',
157 | component: ComponentApi,
158 | },
159 | {
160 | type: 'component',
161 | duration: 9000,
162 | component: HelpText,
163 | },
164 | ];
165 |
166 | return (
167 |
175 |
176 |
177 | );
178 | }
179 |
--------------------------------------------------------------------------------
/packages/documentation/src/components/HeaderStories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Stories from 'stories-react';
3 | import 'stories-react/dist/index.css';
4 |
5 | function Head(props) {
6 | return (
7 |
17 |
18 |
28 |
29 |
34 |
42 | {props.name}
43 |
44 |
52 | {`${props.time} hours ago`}
53 |
54 |
55 |
56 | );
57 | }
58 |
59 | export default function HeaderStories() {
60 | const stories = [
61 | {
62 | type: 'image',
63 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
64 | duration: 15000,
65 | header: (
66 |
71 | ),
72 | },
73 | {
74 | type: 'image',
75 | duration: 6000,
76 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
77 | header: (
78 |
83 | ),
84 | },
85 | {
86 | duration: 7000,
87 | type: 'image',
88 | url: 'https://images.pexels.com/photos/10964888/pexels-photo-10964888.jpeg?w=300',
89 | header: (
90 |
95 | ),
96 | },
97 | {
98 | type: 'image',
99 | url: 'https://images.pexels.com/photos/10985425/pexels-photo-10985425.jpeg?w=300',
100 | duration: 15000,
101 | header: (
102 |
107 | ),
108 | },
109 | ];
110 |
111 | return (
112 |
120 |
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/packages/documentation/src/components/HomepageFeatures.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import clsx from 'clsx';
3 | import styles from './HomepageFeatures.module.css';
4 |
5 | const FeatureList = [
6 | {
7 | title: 'Easy to Use',
8 | url: require('../../static/img/1.jpg').default,
9 | description: (
10 | <>
11 | we designed it from the ground up to be easily usable in your website
12 | with clean and simple api.
13 | >
14 | ),
15 | },
16 | {
17 | title: 'Light weight',
18 | url: require('../../static/img/3.jpg').default,
19 | description: <>its just 3Kb gzipped>,
20 | },
21 | {
22 | title: 'Focus on What Matters',
23 | url: require('../../static/img/2.jpg').default,
24 | description: <>focus on your business logic, leave stories to us !>,
25 | },
26 | ];
27 |
28 | function Feature({ url, title, description }) {
29 | return (
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
{title}
43 |
{description}
44 |
45 |
46 | );
47 | }
48 |
49 | export default function HomepageFeatures() {
50 | return (
51 |
52 |
53 |
54 | {FeatureList.map((props, idx) => (
55 |
56 | ))}
57 |
58 |
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/packages/documentation/src/components/HomepageFeatures.module.css:
--------------------------------------------------------------------------------
1 | .features {
2 | display: flex;
3 | align-items: center;
4 | padding: 2rem 0;
5 | width: 100%;
6 | }
7 |
8 | .featureImgBlock {
9 | height:200px;
10 | text-align: center;
11 | margin-bottom: 16px;
12 | }
13 |
14 | .featureSvg {
15 | height: 100%;
16 | }
17 |
18 | .flexCenter {
19 | display:flex;
20 | justify-content: center;
21 | }
22 |
--------------------------------------------------------------------------------
/packages/documentation/src/components/ImageStories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Stories from 'stories-react';
3 | import 'stories-react/dist/index.css';
4 | import './styles.css';
5 |
6 | export default function ImagesStories() {
7 | const stories = [
8 | {
9 | type: 'image',
10 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
11 | duration: 5000,
12 | },
13 | {
14 | type: 'image',
15 | duration: 6000,
16 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
17 | },
18 |
19 | {
20 | duration: 7000,
21 | type: 'image',
22 | url: 'https://images.pexels.com/photos/10964888/pexels-photo-10964888.jpeg?w=300',
23 | },
24 | {
25 | duration: 7000,
26 | type: 'image',
27 | url: 'https://images.pexels.com/photos/10985425/pexels-photo-10985425.jpeg?w=300',
28 | },
29 | ];
30 |
31 | return (
32 |
40 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/packages/documentation/src/components/ImageVideoStories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Stories from 'stories-react';
3 | import 'stories-react/dist/index.css';
4 |
5 | export default function ImageVideoStories() {
6 | const stories = [
7 | {
8 | type: 'image',
9 | duration: 6000,
10 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
11 | },
12 | {
13 | type: 'video',
14 | url: 'https://assets.mixkit.co/videos/preview/mixkit-man-dancing-under-changing-lights-1240-large.mp4',
15 | duration: 28000,
16 | },
17 | {
18 | type: 'image',
19 | duration: 6000,
20 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
21 | },
22 | {
23 | type: 'video',
24 | url: 'https://assets.mixkit.co/videos/preview/mixkit-mother-with-her-little-daughter-eating-a-marshmallow-in-nature-39764-large.mp4',
25 | duration: 10000,
26 | },
27 | {
28 | type: 'video',
29 | url: 'https://assets.mixkit.co/videos/preview/mixkit-family-walking-together-in-nature-39767-large.mp4',
30 | duration: 10000,
31 | },
32 |
33 | {
34 | type: 'video',
35 | duration: 6000,
36 | url: 'https://assets.mixkit.co/videos/preview/mixkit-girl-in-neon-sign-1232-large.mp4',
37 | },
38 | {
39 | duration: 7000,
40 | type: 'image',
41 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
42 | },
43 | ];
44 | return (
45 |
53 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/packages/documentation/src/components/SeeMoreStories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Stories from 'stories-react';
3 | import 'stories-react/dist/index.css';
4 |
5 | function SeeMoreComponent() {
6 | return (
7 |
16 |
Component opened from see more
17 |
20 |
21 |
22 |
23 | You need to add zindex >= 2
to any interaction u want in
24 | the component
25 |
26 |
28 | window.open('https://www.pexels.com/@imadclicks', '_blank')
29 | }
30 | style={{
31 | color: '#3399FF',
32 | border: '1px solid',
33 | borderColor: '#3399FF',
34 | borderRadius: '3px',
35 | height: '30px',
36 | cursor: 'pointer',
37 | position: 'relative',
38 | zIndex: '2',
39 | width: '100%',
40 | }}
41 | >
42 | Follow Imad Clicks on pexels for amazing pictures
43 |
44 |
45 | );
46 | }
47 |
48 | export default function SeeMoreStories() {
49 | const stories = [
50 | {
51 | type: 'image',
52 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
53 | duration: 5000,
54 | seeMore: true,
55 | seeMoreComponent: SeeMoreComponent,
56 | },
57 | {
58 | type: 'image',
59 | duration: 6000,
60 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
61 | seeMore: 'Custom See More String',
62 | seeMoreComponent: SeeMoreComponent,
63 | },
64 | {
65 | duration: 7000,
66 | type: 'image',
67 | url: 'https://images.pexels.com/photos/10964888/pexels-photo-10964888.jpeg?w=300',
68 | seeMore: (
69 |
70 | custom see more button
71 |
72 | ),
73 | seeMoreComponent: SeeMoreComponent,
74 | },
75 | {
76 | type: 'image',
77 | url: 'https://images.pexels.com/photos/10985425/pexels-photo-10985425.jpeg?w=300',
78 | duration: 5000,
79 | seeMore: true,
80 | seeMoreComponent: SeeMoreComponent,
81 | },
82 | ];
83 |
84 | return (
85 |
93 |
94 |
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/packages/documentation/src/components/VideoStories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Stories from 'stories-react';
3 | import 'stories-react/dist/index.css';
4 |
5 | export default function VideoStories() {
6 | const stories = [
7 | {
8 | type: 'video',
9 | url: 'https://assets.mixkit.co/videos/preview/mixkit-man-dancing-under-changing-lights-1240-large.mp4',
10 | duration: 28000,
11 | },
12 | {
13 | type: 'video',
14 | url: 'https://assets.mixkit.co/videos/preview/mixkit-mother-with-her-little-daughter-eating-a-marshmallow-in-nature-39764-large.mp4',
15 | duration: 10000,
16 | },
17 | {
18 | type: 'video',
19 | url: 'https://assets.mixkit.co/videos/preview/mixkit-family-walking-together-in-nature-39767-large.mp4',
20 | duration: 10000,
21 | },
22 | {
23 | type: 'video',
24 | duration: 6000,
25 | url: 'https://assets.mixkit.co/videos/preview/mixkit-girl-in-neon-sign-1232-large.mp4',
26 | },
27 | {
28 | duration: 30000,
29 | type: 'video',
30 | url: 'https://assets.mixkit.co/videos/preview/mixkit-tree-with-yellow-flowers-1173-large.mp4',
31 | },
32 | ];
33 |
34 | return (
35 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/packages/documentation/src/components/styles.css:
--------------------------------------------------------------------------------
1 | .custom-container,
2 | .custom-progress-container,
3 | .custom-progress-bar-container,
4 | .custon-progress-bar,
5 | .custom-story-container {
6 | font-size: 1px;
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/packages/documentation/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Any CSS included here will be global. The classic template
3 | * bundles Infima by default. Infima is a CSS framework designed to
4 | * work well for content-centric websites.
5 | */
6 |
7 | /* You can override the default Infima variables here. */
8 | :root {
9 | --ifm-color-primary: #25c2a0;
10 | --ifm-color-primary-dark: rgb(33, 175, 144);
11 | --ifm-color-primary-darker: rgb(31, 165, 136);
12 | --ifm-color-primary-darkest: rgb(26, 136, 112);
13 | --ifm-color-primary-light: rgb(70, 203, 174);
14 | --ifm-color-primary-lighter: rgb(102, 212, 189);
15 | --ifm-color-primary-lightest: rgb(146, 224, 208);
16 | --ifm-code-font-size: 95%;
17 | }
18 |
19 | .docusaurus-highlight-code-line {
20 | background-color: rgba(0, 0, 0, 0.1);
21 | display: block;
22 | margin: 0 calc(-1 * var(--ifm-pre-padding));
23 | padding: 0 var(--ifm-pre-padding);
24 | }
25 |
26 | html[data-theme='dark'] .docusaurus-highlight-code-line {
27 | background-color: rgba(0, 0, 0, 0.3);
28 | }
29 |
--------------------------------------------------------------------------------
/packages/documentation/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import clsx from 'clsx';
3 | import Layout from '@theme/Layout';
4 | import Link from '@docusaurus/Link';
5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
6 | import styles from './index.module.css';
7 | import HomepageFeatures from '../components/HomepageFeatures';
8 |
9 | function HomepageHeader() {
10 | const { siteConfig } = useDocusaurusContext();
11 | return (
12 |
26 | );
27 | }
28 |
29 | export default function Home() {
30 | const { siteConfig } = useDocusaurusContext();
31 | return (
32 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/packages/documentation/src/pages/index.module.css:
--------------------------------------------------------------------------------
1 | /**
2 | * CSS files with the .module.css suffix will be treated as CSS modules
3 | * and scoped locally.
4 | */
5 |
6 | .heroBanner {
7 | padding: 4rem 0;
8 | text-align: center;
9 | position: relative;
10 | overflow: hidden;
11 | }
12 |
13 | @media screen and (max-width: 966px) {
14 | .heroBanner {
15 | padding: 2rem;
16 | }
17 | }
18 |
19 | .buttons {
20 | display: flex;
21 | align-items: center;
22 | justify-content: center;
23 | }
24 |
--------------------------------------------------------------------------------
/packages/documentation/src/pages/markdown-page.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Markdown page example
3 | ---
4 |
5 | # Markdown page example
6 |
7 | You don't need React to write simple standalone pages.
8 |
--------------------------------------------------------------------------------
/packages/documentation/src/theme/ReactLiveScope/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | import React from 'react';
9 | import Stories from 'stories-react';
10 | import storyStyles from 'stories-react';
11 |
12 | // Add react-live imports you need here
13 | const ReactLiveScope = {
14 | React,
15 | ...React,
16 | Stories,
17 | storyStyles,
18 | };
19 |
20 | export default ReactLiveScope;
21 |
--------------------------------------------------------------------------------
/packages/documentation/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/packages/documentation/static/.nojekyll
--------------------------------------------------------------------------------
/packages/documentation/static/img/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/packages/documentation/static/img/1.jpg
--------------------------------------------------------------------------------
/packages/documentation/static/img/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/packages/documentation/static/img/2.jpg
--------------------------------------------------------------------------------
/packages/documentation/static/img/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/packages/documentation/static/img/3.jpg
--------------------------------------------------------------------------------
/packages/documentation/static/img/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/packages/documentation/static/img/android-chrome-192x192.png
--------------------------------------------------------------------------------
/packages/documentation/static/img/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/packages/documentation/static/img/android-chrome-512x512.png
--------------------------------------------------------------------------------
/packages/documentation/static/img/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/packages/documentation/static/img/apple-touch-icon.png
--------------------------------------------------------------------------------
/packages/documentation/static/img/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/packages/documentation/static/img/favicon-16x16.png
--------------------------------------------------------------------------------
/packages/documentation/static/img/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/packages/documentation/static/img/favicon-32x32.png
--------------------------------------------------------------------------------
/packages/documentation/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/packages/documentation/static/img/favicon.ico
--------------------------------------------------------------------------------
/packages/documentation/static/img/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/packages/documentation/static/img/logo.jpg
--------------------------------------------------------------------------------
/packages/documentation/static/img/logo.svg_:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/documentation/static/img/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/packages/documentation/static/img/tutorial/docsVersionDropdown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/packages/documentation/static/img/tutorial/docsVersionDropdown.png
--------------------------------------------------------------------------------
/packages/documentation/static/img/tutorial/localeDropdown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hannadrehman/stories-react/368e97e7cf64394da9f218b007e10079129ab5c3/packages/documentation/static/img/tutorial/localeDropdown.png
--------------------------------------------------------------------------------
/packages/stories/README.md:
--------------------------------------------------------------------------------
1 |
stories-react
2 |
A React component For Instagram like stories
3 |
4 |
Homepage
5 |
6 |
11 |
22 |
30 |
38 |
39 |
40 |
41 |
42 |
43 | # Install
44 |
45 | ```sh
46 | npm install --save stories-react
47 | ```
48 |
49 | # Demo
50 |
51 | You can find working demo [here](https://hannadrehman.github.io/stories-react/)
52 |
53 | # Usage
54 |
55 | ```jsx
56 | import React from 'react';
57 | import Stories from 'stories-react';
58 | import 'stories-react/dist/index.css';
59 |
60 | export default function ImagesStories() {
61 | const stories = [
62 | {
63 | type: 'image',
64 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
65 | duration: 5000,
66 | },
67 | {
68 | type: 'image',
69 | duration: 6000,
70 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
71 | },
72 | {
73 | duration: 7000,
74 | type: 'image',
75 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
76 | },
77 | ];
78 |
79 | return ;
80 | }
81 | ```
82 |
83 | ## Props
84 |
85 | | Property | Type | Default | Description |
86 | | ------------------------------ | ------------------------ | ------- | ----------------------------------------------------------------------------------- |
87 | | `stories` | `IStoryObject[]` | `[]` | An array of story objects. description of `IStoryObject` is mentioned below |
88 | | `height` | `string` | `100%` | Height of story container |
89 | | `width` | `string` | `100%` | Width of story container |
90 | | `onStoryChange` | `function(index:number)` | `-` | Callback called when story changes |
91 | | `onStoriesStart` | `function` | `-` | Callback called when first story is rendered. it get called only once, |
92 | | `onAllStoriesEnd` | `function` | `-` | Callback called when last story gets completed. it will get called only once |
93 | | `currentIndex` | `number` | `-` | Current index of the story which should be selected first |
94 | | `defaultDuration` | `number` | `10000` | default duration in ms of stories if duration is not provided in the `IStoryObject` |
95 | | `classNames` | `IStoryClassNames` | `{}` | classnames to overide different sections of a story renderer |
96 | | `pauseStoryWhenInActiveWindow` | `boolean` | `true` | pauses story when window goes out of focus (user changes tab/minimizes browser etc |
97 |
98 | ## IStoryObject
99 |
100 | | Property | Type | Default | Description |
101 | | ------------------ | ------------------------------------ | ------- | ------------------------------------------------ |
102 | | `type` | `image , video , component` | `-` | type of story to render |
103 | | `url` | `string` | `-` | url of a story (image & video only) |
104 | | `duration` | `duration` | `10000` | duration in ms of the story |
105 | | `component` | `React Component` | `-` | custom component to render as a story |
106 | | `header` | `React Component` | `-` | header of a story |
107 | | `seeMore` | `string , boolean , React Component` | `true` | See more action text |
108 | | `seeMoreComponent` | `React Component` | `-` | see more component opens after clicking see more |
109 | | `onSeeMoreClick` | `function(index:number)` | `-` | Callback called when story see more is clicked |
110 |
111 | ## IStoryClassNames
112 |
113 | | Property | Type | Default | Description |
114 | | ---------------------- | -------- | ------- | ----------------------------------------------- |
115 | | `main` | `string` | `-` | classname for main container |
116 | | `progressContainer` | `string` | `-` | classname for progress line container |
117 | | `progressBarContainer` | `string` | `-` | classname for single progress bar box container |
118 | | `progressBar` | `string` | `-` | classname for progress bar |
119 | | `storyContainer` | `string` | `-` | classname for story container |
120 |
121 | ## Custom Component Story Props
122 |
123 | | Property | Type | | Description |
124 | | ---------- | -------------- | --- | ------------------------- |
125 | | `pause` | `function` | | call it to pause a story |
126 | | `resume` | `function` | | call it to resume a story |
127 | | `story` | `IStoryObject` | | current story properties |
128 | | `isPaused` | `boolean` | | state of a story |
129 |
--------------------------------------------------------------------------------
/packages/stories/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stories-react",
3 | "version": "1.2.0",
4 | "description": "instagram style stories in react js",
5 | "main": "dist/index.js",
6 | "module": "dist/index.esm.js",
7 | "typings": "dist/index.d.ts",
8 | "scripts": {
9 | "test": "yarn test",
10 | "build": "rollup -c",
11 | "build:watch": "yarn build -w"
12 | },
13 | "keywords": [
14 | "reactjs",
15 | "typescript",
16 | "instagram",
17 | "stories"
18 | ],
19 | "repository": "https://github.com/hannadrehman/stories-react/",
20 | "homepage": "https://hannadrehman.github.io/stories-react/",
21 | "bugs": "https://github.com/hannadrehman/stories-react/issues",
22 | "author": {
23 | "name": "Hannad Rehman",
24 | "email": "hannad63@gmail.com",
25 | "url": "https://hannadrehman.com"
26 | },
27 | "license": "MIT",
28 | "peerDependencies": {
29 | "react": "^18.2.0",
30 | "react-dom": "^18.2.0"
31 | },
32 | "devDependencies": {
33 | "@babel/preset-typescript": "^7.21.0",
34 | "@rollup/plugin-commonjs": "^24.0.1",
35 | "@rollup/plugin-node-resolve": "^15.0.1",
36 | "@types/jest": "^29.4.0",
37 | "@types/node": "^18.14.6",
38 | "@types/react": "^18.0.28",
39 | "@types/react-dom": "^18.0.11",
40 | "cssnano": "^5.1.15",
41 | "postcss-preset-env": "^8.0.1",
42 | "rollup": "^3.18.0",
43 | "rollup-plugin-css-only": "^4.3.0",
44 | "rollup-plugin-peer-deps-external": "^2.2.4",
45 | "rollup-plugin-postcss": "^4.0.2",
46 | "rollup-plugin-terser": "^7.0.2",
47 | "rollup-plugin-typescript2": "^0.34.1",
48 | "typescript": "^4.9.5"
49 | },
50 | "dependencies": {}
51 | }
52 |
--------------------------------------------------------------------------------
/packages/stories/rollup.config.js:
--------------------------------------------------------------------------------
1 | const peerDepsExternal = require('rollup-plugin-peer-deps-external');
2 | const resolve = require('@rollup/plugin-node-resolve');
3 | const commonjs = require('@rollup/plugin-commonjs');
4 | const typescript = require('rollup-plugin-typescript2');
5 | const { terser } = require('rollup-plugin-terser');
6 | const postcss = require('rollup-plugin-postcss');
7 | const postcssPresetEnv = require('postcss-preset-env');
8 | const cssnano = require('cssnano');
9 | const packageJson = require('./package.json');
10 |
11 | module.exports = {
12 | input: 'src/index.tsx',
13 | output: [
14 | {
15 | file: packageJson.main,
16 | format: 'cjs',
17 | sourcemap: true,
18 | },
19 | {
20 | file: packageJson.module,
21 | format: 'esm',
22 | sourcemap: true,
23 | },
24 | ],
25 | plugins: [
26 | peerDepsExternal(),
27 | resolve(),
28 | commonjs(),
29 | typescript({
30 | tsconfig: './tsconfig.json',
31 | }),
32 | postcss({
33 | extensions: ['.css'],
34 | modules: true,
35 | extract: true,
36 | minimize: true,
37 | plugins: [postcssPresetEnv({ stage: 0 }), cssnano()],
38 | }),
39 | terser({
40 | output: {
41 | comments: false,
42 | },
43 | }),
44 | ],
45 | };
46 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Actions/Actions.component.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment, useRef, useState } from 'react';
2 | import * as CONSTANTS from './Actions.constants';
3 | import styles from './Actions.styles.css';
4 |
5 | interface IActionsProps {
6 | onNextClick: () => void;
7 | onPrevClick: () => void;
8 | onPause: () => void;
9 | onResume: () => void;
10 | }
11 |
12 | type IActionEvent = React.MouseEvent | React.TouchEvent;
13 |
14 | export function Actions({
15 | onNextClick,
16 | onPrevClick,
17 | onPause,
18 | onResume,
19 | }: IActionsProps) {
20 | const [isStoryPaused, setIsStoryPaused] = useState(false);
21 | //adding pause timer because we want to debouce pause interaction
22 | //because mouse down is called with mouse up immediately
23 | const pauseTimerRef = useRef(null);
24 |
25 | function handlePause(event: IActionEvent) {
26 | event.stopPropagation();
27 | event.preventDefault();
28 | clearTimeout(pauseTimerRef.current);
29 |
30 | // delay this transaction
31 | pauseTimerRef.current = setTimeout(() => {
32 | onPause();
33 | setIsStoryPaused(true);
34 | }, 200);
35 | }
36 |
37 | function handleInteractions(region: string, event: IActionEvent) {
38 | event.stopPropagation();
39 | event.preventDefault();
40 |
41 | //clear any pending timeout
42 | clearTimeout(pauseTimerRef.current);
43 | if (isStoryPaused) {
44 | onResume();
45 | setIsStoryPaused(false);
46 | return;
47 | }
48 | onResume();
49 | if (region == CONSTANTS.EVENT_REGION.LEFT) {
50 | onPrevClick();
51 | return;
52 | }
53 | onNextClick();
54 | }
55 |
56 | function getEvents(region: string) {
57 | return {
58 | onMouseUp: (e: React.MouseEvent) => handleInteractions(region, e),
59 | onTouchEnd: (e: React.TouchEvent) => handleInteractions(region, e),
60 | onTouchStart: (e: React.TouchEvent) => handlePause(e),
61 | onMouseDown: (e: React.MouseEvent) => handlePause(e),
62 | };
63 | }
64 |
65 | return (
66 |
67 |
71 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Actions/Actions.constants.ts:
--------------------------------------------------------------------------------
1 | export const EVENT_TYPE = Object.freeze({
2 | MOUSE_DOWN: 'mouseDown',
3 | MOUSE_UP: 'mouseUp',
4 | TOUCH_START: 'touchStart',
5 | TOUCH_END: 'touchEnd',
6 | });
7 |
8 | export const EVENT_REGION = Object.freeze({
9 | LEFT: 'left',
10 | RIGHT: 'right',
11 | });
12 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Actions/Actions.styles.css:
--------------------------------------------------------------------------------
1 | .left {
2 | position: absolute;
3 | width: 50%;
4 | top: 0;
5 | bottom: 0;
6 | height: 100%;
7 | z-index: 1;
8 | left: 0;
9 |
10 | }
11 |
12 | .right {
13 | position: absolute;
14 | width: 50%;
15 | top: 0;
16 | bottom: 0;
17 | height: 100%;
18 | z-index: 1;
19 | right: 0;
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Actions/index.ts:
--------------------------------------------------------------------------------
1 | import { Actions } from './Actions.component';
2 |
3 | export { Actions };
4 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/CustomComponent/CustomComponents.component.tsx:
--------------------------------------------------------------------------------
1 | import styles from './CustomComponents.styles.css';
2 | import { IStoryComponentProps } from '../../types';
3 |
4 | export function CustomComponent(props: IStoryComponentProps) {
5 | return (
6 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/CustomComponent/CustomComponents.styles.css:
--------------------------------------------------------------------------------
1 | .component {
2 | width: 100%;
3 | height: 100%;
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/CustomComponent/index.ts:
--------------------------------------------------------------------------------
1 | import { CustomComponent } from './CustomComponents.component';
2 |
3 | export { CustomComponent };
4 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Image/Image.component.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { IStoryComponentProps } from '../../types';
3 | import styles from './Image.styles.css';
4 |
5 | export function Image(props: IStoryComponentProps) {
6 | useEffect(() => {
7 | props.onPause();
8 | }, []);
9 |
10 | function handleLoadImage() {
11 | //set timeout is done because there is an inconsitancy in safari and other browser
12 | //on when to call useEffect
13 | setTimeout(() => {
14 | props.onResume();
15 | }, 4);
16 | }
17 |
18 | return (
19 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Image/Image.styles.css:
--------------------------------------------------------------------------------
1 | .image {
2 | height: 100%;
3 | width: 100%;
4 | object-fit: cover;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Image/index.ts:
--------------------------------------------------------------------------------
1 | import { Image } from './Image.component';
2 |
3 | export { Image };
4 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Progress/Progress.component.tsx:
--------------------------------------------------------------------------------
1 | import { useStoriesContext } from '../../Hooks';
2 | import { ProgressBar } from '../ProgressBar';
3 | import { IStoryIndexedObject } from '../../types';
4 | import styles from './progress.styles.css';
5 |
6 | interface IProgressProps {
7 | activeStoryIndex: number;
8 | isPaused: boolean;
9 | }
10 |
11 | export function Progress(props: IProgressProps) {
12 | const { stories, classNames } = useStoriesContext();
13 | return (
14 |
18 | {stories.map((story: IStoryIndexedObject) => (
19 |
26 | ))}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Progress/index.ts:
--------------------------------------------------------------------------------
1 | import { Progress } from './Progress.component';
2 |
3 | export { Progress };
4 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Progress/progress.styles.css:
--------------------------------------------------------------------------------
1 | .wrapper{
2 | position: absolute;
3 | left: 0;
4 | right: 0;
5 | top: 0;
6 | height: 2px;
7 | display: grid;
8 | grid-gap: 4px;
9 | padding: 4px;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/ProgressBar/ProgressBar.component.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 | import * as hooks from '../../Hooks/';
3 | import { IStoryIndexedObject } from '../../types';
4 | import styles from './ProgressBar.styles.css';
5 |
6 | interface IProgressBarProps {
7 | hasStoryPassed: boolean;
8 | isActive: boolean;
9 | story: IStoryIndexedObject;
10 | isPaused: boolean;
11 | }
12 |
13 | let barWidth = 0; // declaring it here to avoid variable creating in a loop. this will improve memory utilization.
14 | let step = 0.1;
15 |
16 | export function ProgressBar(props: IProgressBarProps) {
17 | const { defaultDuration, classNames } = hooks.useStoriesContext();
18 | const barRef = useRef(null);
19 | const barWrapperRef = useRef(null);
20 | const [shouldAnimate, setShouldAnimate] = useState(false);
21 |
22 | //set animations
23 | useEffect(() => {
24 | if (props.isPaused || !props.isActive) {
25 | setShouldAnimate(false);
26 | return;
27 | }
28 | if (props.isActive) {
29 | setShouldAnimate(true);
30 | return;
31 | }
32 | setShouldAnimate(false);
33 | }, [props.isActive, props.isPaused]);
34 |
35 | useEffect(() => {
36 | if (!barRef.current) {
37 | return;
38 | }
39 | if (props.hasStoryPassed) {
40 | barRef.current.style.width = `${barWrapperRef?.current?.offsetWidth}px`;
41 | return;
42 | }
43 | barRef.current.style.width = '0px';
44 | }, [props.hasStoryPassed, props.isActive]);
45 |
46 | hooks.useAnimationFrame((time: number) => {
47 | if (!barRef.current || !barWrapperRef.current) {
48 | return;
49 | }
50 | barWidth =
51 | Number(
52 | (barRef.current.style.width || '1px').slice(
53 | 0,
54 | barRef.current.style.width.length - 2,
55 | ),
56 | ) || 0;
57 |
58 | if (barWidth > barWrapperRef.current.offsetWidth) {
59 | setShouldAnimate(false);
60 | return;
61 | }
62 |
63 | step =
64 | barWrapperRef?.current?.offsetWidth /
65 | ((props.story.duration || defaultDuration) / time);
66 | barRef.current.style.width = `${barWidth + step}px`;
67 | }, shouldAnimate);
68 |
69 | return (
70 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/ProgressBar/ProgressBar.styles.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | position: relative;
3 | background-color: #6a6a6a;
4 | border-radius: 2px;
5 | height:2px;
6 | }
7 |
8 | .bar {
9 | width: 0;
10 | position: absolute;
11 | left: 0;
12 | right: 0;
13 | top: 0;
14 | bottom: 0;
15 | border-radius: 2px;
16 | background-color: #eae8e8;
17 | height:2px;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/ProgressBar/index.ts:
--------------------------------------------------------------------------------
1 | import { ProgressBar } from './ProgressBar.component';
2 |
3 | export { ProgressBar };
4 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/SeeMore/SeeMore.component.tsx:
--------------------------------------------------------------------------------
1 | import { IStoryIndexedObject } from '../../types';
2 | import styles from './SeeMore.styles.css';
3 |
4 | interface Iprops {
5 | story: IStoryIndexedObject;
6 | onSeeMoreClick: () => void;
7 | }
8 | export function SeeMore(props: Iprops) {
9 | function getSeeMore() {
10 | const seeMore = props.story.seeMore;
11 | const typeOfSeeMore = typeof seeMore;
12 | if (['string', 'boolean'].includes(typeOfSeeMore)) {
13 | const seeMoreText = typeOfSeeMore === 'string' ? seeMore : 'See More';
14 | return (
15 |
16 |
^
17 |
{seeMoreText}
18 |
19 | );
20 | }
21 | if (typeOfSeeMore === 'function') {
22 | return ;
23 | }
24 | return props.story.seeMore;
25 | }
26 |
27 | function handleSeeMore() {
28 | props.onSeeMoreClick();
29 | }
30 |
31 | if (!props.story.seeMore) {
32 | return null;
33 | }
34 |
35 | return (
36 |
41 | {getSeeMore()}
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/SeeMore/SeeMore.styles.css:
--------------------------------------------------------------------------------
1 | .seeMoreWrapper {
2 | position:absolute;
3 | bottom:24px;
4 | left:0;
5 | right:0;
6 | display:flex;
7 | justify-content:center;
8 | width: 100%;
9 | outline: none;
10 | background-color: inherit;
11 | border:none;
12 | outline:none;
13 | z-index: 2;
14 | cursor: pointer;
15 | }
16 |
17 | .defaultSeeMore{
18 | display:flex;
19 | width: 100%;
20 | flex-direction: column;
21 | justify-content: center;
22 | align-items:center;
23 | color:white;
24 | font-weight: 600;
25 | font-size: 14px;
26 | line-height: 0.8;
27 | }
28 |
29 | .defaultSeeMore p {
30 | margin:0;
31 | padding:0;
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/SeeMore/index.ts:
--------------------------------------------------------------------------------
1 | import { SeeMore } from './SeeMore.component';
2 |
3 | export { SeeMore };
4 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/SeeMoreComponent/SeeMoreComponent.component.tsx:
--------------------------------------------------------------------------------
1 | import { IStoryIndexedObject } from '../../types';
2 | import styles from './SeeMoreComponent.styles.css';
3 |
4 | interface IProps {
5 | story: IStoryIndexedObject;
6 | onClose: () => void;
7 | }
8 |
9 | export function SeeMoreComponent(props: IProps) {
10 | function getSeeMoreComponent() {
11 | if (typeof props.story.seeMoreComponent === 'function') {
12 | return ;
13 | }
14 | return props.story.seeMoreComponent;
15 | }
16 | if (!props.story.seeMore) {
17 | return null;
18 | }
19 | if (!props.story.seeMoreComponent) {
20 | return null;
21 | }
22 |
23 | return (
24 |
25 |
26 | ✕
27 |
28 | {getSeeMoreComponent()}
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/SeeMoreComponent/SeeMoreComponent.styles.css:
--------------------------------------------------------------------------------
1 | .seeMoreComponentWrapper {
2 | background-color: white;
3 | max-height: 100%;
4 | overflow-y: auto;
5 | position: absolute;
6 | left:0;
7 | right:0;
8 | z-index:3;
9 | animation: up 200ms ease-in-out;
10 | animation-fill-mode: forwards;
11 | top: 0;
12 | bottom: 0;
13 | }
14 |
15 | .closeIcon {
16 | position: absolute;
17 | top:0;
18 | padding:16px;
19 | outline: none;
20 | background-color: inherit;
21 | border:none;
22 | outline:none;
23 | cursor: pointer;
24 | font-size: 16px;
25 | right: 0;
26 | top: 0;
27 | background: transparent;
28 |
29 | }
30 |
31 | @keyframes up {
32 | from {transform: translateY(10%); }
33 | to{transform: translateY(0);}
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/SeeMoreComponent/index.ts:
--------------------------------------------------------------------------------
1 | import { SeeMoreComponent } from './SeeMoreComponent.component';
2 |
3 | export { SeeMoreComponent };
4 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/SoundIcon/SoundIcon.component.tsx:
--------------------------------------------------------------------------------
1 | interface IProps {
2 | type: string;
3 | }
4 |
5 | export function SoundIcon(props: IProps) {
6 | if (props.type === 'off') {
7 | return (
8 |
15 |
16 |
17 | );
18 | }
19 |
20 | return (
21 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/SoundIcon/index.ts:
--------------------------------------------------------------------------------
1 | import { SoundIcon } from './SoundIcon.component';
2 |
3 | export { SoundIcon };
4 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Story/Story.component.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 | import * as CONSTANTS from './Story.constants';
3 | import styles from './Story.styles.css';
4 | import { IStoryComponentProps } from '../../types';
5 | import { Image } from '../Image';
6 | import { Video } from '../Video';
7 | import { CustomComponent } from '../CustomComponent';
8 | import { SeeMore } from '../SeeMore';
9 | import { SeeMoreComponent } from '../SeeMoreComponent';
10 | import * as hooks from '../../Hooks';
11 |
12 | export function Story(props: IStoryComponentProps) {
13 | const [showSeeMoreComponent, setShowSeeMoreComponent] = useState(false);
14 | const { classNames } = hooks.useStoriesContext();
15 |
16 | useEffect(() => {
17 | setShowSeeMoreComponent(false);
18 | }, [props.story]);
19 |
20 | function getStory() {
21 | if (props.story.type === CONSTANTS.STORY_TYPES.IMAGE) {
22 | return ;
23 | }
24 | if (props.story.type === CONSTANTS.STORY_TYPES.VIDEO) {
25 | return ;
26 | }
27 | if (props.story.type === CONSTANTS.STORY_TYPES.COMPONENT) {
28 | return ;
29 | }
30 |
31 | return null;
32 | }
33 |
34 | function getHeader() {
35 | if (typeof props.story.header === 'function') {
36 | return ;
37 | }
38 | return props.story.header;
39 | }
40 |
41 | function handleSeeMore() {
42 | props.onPause();
43 | setShowSeeMoreComponent(true);
44 | props.story.onSeeMoreClick?.(props.story.index);
45 | }
46 |
47 | function handleCloseSeeMore() {
48 | props.onResume();
49 | setShowSeeMoreComponent(false);
50 | }
51 | return (
52 |
53 | {getStory()}
54 | {props.story.header &&
{getHeader()}
}
55 |
56 | {showSeeMoreComponent && (
57 |
58 | )}
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Story/Story.constants.ts:
--------------------------------------------------------------------------------
1 | export const STORY_TYPES = Object.freeze({
2 | IMAGE: 'image',
3 | VIDEO: 'video',
4 | COMPONENT: 'component',
5 | });
6 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Story/Story.styles.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | width: 100%;
3 | height: 100%;
4 | display: flex;
5 | align-content: center;
6 | align-items: center;
7 | }
8 |
9 | .header {
10 | position:absolute;
11 | top:12px;
12 | left:0;
13 | right:0
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Story/index.ts:
--------------------------------------------------------------------------------
1 | import { Story } from './Story.component';
2 |
3 | export { Story };
4 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Video/Video.component.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment, useEffect, useRef, useState } from 'react';
2 | import styles from './Video.styles.css';
3 | import { IStoryComponentProps } from '../../types';
4 | import * as hooks from '../../Hooks';
5 | import { SoundIcon } from '../SoundIcon';
6 |
7 | const key = 'RSIsMute';
8 | const WINDOW: any = typeof window === 'undefined' ? {} : window;
9 | WINDOW?.localStorage?.setItem(
10 | key,
11 | WINDOW?.localStorage?.getItem(key) ? WINDOW?.localStorage?.getItem(key) : 'true'
12 | );
13 |
14 | export function Video(props: IStoryComponentProps) {
15 | const { isPaused } = hooks.useStoriesContext();
16 | const [isMuted, setIsMuted] = useState(
17 | WINDOW?.localStorage?.getItem(key) === 'true'
18 | );
19 | const [showLoader, setShowLoader] = useState(false);
20 | const videoRef = useRef(null);
21 |
22 | function setMute(value: boolean) {
23 | WINDOW?.localStorage?.setItem(key, String(value));
24 | setIsMuted(value);
25 | }
26 |
27 | useEffect(() => {
28 | props.onPause();
29 | setShowLoader(true);
30 | }, []);
31 |
32 | useEffect(() => {
33 | if (!videoRef.current) {
34 | return;
35 | }
36 | if (isPaused && !videoRef.current.paused) {
37 | videoRef.current.pause();
38 | return;
39 | }
40 | videoRef.current.play().catch(() => {
41 | videoRef.current?.play();
42 | });
43 | }, [isPaused]);
44 |
45 | function handleLoad() {
46 | setTimeout(() => {
47 | props.onResume();
48 | setShowLoader(false);
49 | }, 4);
50 | }
51 | return (
52 |
53 |
63 |
64 |
65 |
66 | Video not supported
67 |
68 | setMute(!isMuted)}>
69 |
70 |
71 | {showLoader && (
72 |
75 | )}
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Video/Video.styles.css:
--------------------------------------------------------------------------------
1 | .video {
2 | max-height: 100%;
3 | max-width: 100%;
4 | width: 100%;
5 | object-fit: cover;
6 | }
7 |
8 | .loaderWrapper {
9 | position: absolute;
10 | top:0;
11 | left:0;
12 | bottom:0;
13 | right:0;
14 | display:flex;
15 | align-items: center;
16 | justify-content: center;
17 | align-content: center;
18 |
19 | }
20 |
21 | .loader {
22 | border: 4px solid #f3f3f3;
23 | border-radius: 50%;
24 | border-top: 4px solid #a5b0b7;
25 | width: 40px;
26 | height: 40px;
27 | animation: spin 1s linear infinite;
28 | }
29 |
30 | .soundIcon {
31 | position: absolute;
32 | padding:16px;
33 | top:0;
34 | right:0;
35 | z-index:2;
36 | background: transparent;
37 | outline: none;
38 | border: none;
39 | cursor: pointer;
40 |
41 | }
42 |
43 | @keyframes spin {
44 | 0% { transform: rotate(0deg); }
45 | 100% { transform: rotate(360deg); }
46 | }
47 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/Video/index.ts:
--------------------------------------------------------------------------------
1 | import { Video } from './Video.component';
2 |
3 | export { Video };
4 |
--------------------------------------------------------------------------------
/packages/stories/src/Components/index.ts:
--------------------------------------------------------------------------------
1 | import { Actions } from './Actions';
2 | import { Progress } from './Progress';
3 | import { Story } from './Story';
4 |
5 | export { Actions, Progress, Story };
6 |
--------------------------------------------------------------------------------
/packages/stories/src/Contexts/index.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 | import { IStoryContext } from '../types';
3 |
4 | const defaultStoryContext: IStoryContext = {
5 | stories: [],
6 | width: '100%',
7 | height: '100%',
8 | defaultDuration: 10000,
9 | isPaused: false,
10 | };
11 |
12 | export const StoriesContext = createContext(defaultStoryContext);
13 |
--------------------------------------------------------------------------------
/packages/stories/src/Hooks/index.ts:
--------------------------------------------------------------------------------
1 | import { useContext, useRef, useEffect } from 'react';
2 | import { StoriesContext } from '../Contexts';
3 | import { IStoryContext } from '../types';
4 |
5 | interface IUseRef {
6 | current: any;
7 | }
8 |
9 | export function useStoriesContext() {
10 | const context: IStoryContext = useContext(StoriesContext);
11 | return context;
12 | }
13 |
14 | export function usePausableTimeout(
15 | callback: () => void,
16 | delay: number | null,
17 | pause: boolean,
18 | ) {
19 | const savedCallback: IUseRef = useRef();
20 | const timeRemaining: IUseRef = useRef(delay);
21 | const startTimeRef: IUseRef = useRef(Date.now());
22 | // Remember the latest callback.
23 | useEffect(() => {
24 | savedCallback.current = callback;
25 | }, [callback]);
26 |
27 | // reset time remaining everytime delay changes
28 | useEffect(() => {
29 | timeRemaining.current = delay;
30 | }, [delay]);
31 |
32 | // Set up the interval.
33 | useEffect(() => {
34 | function tick() {
35 | savedCallback.current();
36 | }
37 | if (delay !== null && pause === false) {
38 | startTimeRef.current = Date.now();
39 | const timerId = setTimeout(tick, timeRemaining.current);
40 | return () => {
41 | clearTimeout(timerId);
42 | };
43 | }
44 | return () => {};
45 | }, [delay, pause]);
46 |
47 | useEffect(() => {
48 | if (pause) {
49 | timeRemaining.current =
50 | timeRemaining.current - (Date.now() - startTimeRef.current);
51 | }
52 | }, [pause]);
53 | }
54 |
55 | export function useAnimationFrame(
56 | callback: (time: number) => void,
57 | start: boolean,
58 | ) {
59 | const requestRef: IUseRef = useRef();
60 | const previousTimeRef: IUseRef = useRef();
61 | const callBackRef: IUseRef = useRef(callback);
62 |
63 | useEffect(() => {
64 | callBackRef.current = callback;
65 | }, [callback]);
66 |
67 | useEffect(() => {
68 | function animate(time: number) {
69 | if (previousTimeRef.current != undefined) {
70 | const deltaTime = time - previousTimeRef.current;
71 | callBackRef.current(deltaTime);
72 | }
73 | previousTimeRef.current = time;
74 | requestRef.current = requestAnimationFrame(animate);
75 | }
76 | if (start !== false) {
77 | requestRef.current = requestAnimationFrame(animate);
78 | return () => {
79 | cancelAnimationFrame(requestRef.current);
80 | requestRef.current = null;
81 | previousTimeRef.current = null;
82 | };
83 | }
84 | return () => {
85 | if (requestRef.current) {
86 | requestRef.current = null;
87 | }
88 | cancelAnimationFrame(requestRef.current);
89 | previousTimeRef.current = null;
90 | };
91 | }, [start]);
92 | }
93 |
94 | export function useWindowVisibility(callback: (isVisible: boolean) => void) {
95 | const callBackRef = useRef(callback);
96 | useEffect(() => {
97 | callBackRef.current = callback;
98 | }, [callback]);
99 |
100 | useEffect(() => {
101 | function handleActive() {
102 | callBackRef.current(true);
103 | }
104 | function handleInActive() {
105 | callBackRef.current(false);
106 | }
107 | window.addEventListener('focus', handleActive);
108 | window.addEventListener('blur', handleInActive);
109 |
110 | return () => {
111 | window.removeEventListener('focus', handleActive);
112 | window.removeEventListener('blur', handleInActive);
113 | };
114 | }, []);
115 | }
116 |
--------------------------------------------------------------------------------
/packages/stories/src/declerations.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css';
2 |
--------------------------------------------------------------------------------
/packages/stories/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 | import { StoriesContext } from './Contexts';
3 | import { Actions, Progress, Story } from './Components';
4 | import { IStoryProps, IStoryIndexedObject, IStoryContext } from './types';
5 | import { useEffect, useMemo, useState } from 'react';
6 | import * as hooks from './Hooks';
7 | import styles from './styles.css';
8 | import * as utilities from './utilities';
9 |
10 | export default function Stories({
11 | stories = [],
12 | width = '100%',
13 | height = '100%',
14 | onStoryChange = () => {},
15 | currentIndex = 0,
16 | defaultDuration = 10000,
17 | loop = false,
18 | onAllStoriesEnd = () => {},
19 | onStoriesStart = () => {},
20 | classNames = {},
21 | pauseStoryWhenInActiveWindow = true,
22 | }: IStoryProps): JSX.Element | null {
23 | const storiesWithIndex: IStoryIndexedObject[] = useMemo(() => {
24 | return utilities.transformStories(stories, defaultDuration);
25 | }, [stories, defaultDuration]);
26 |
27 | const [selectedStory, setSelectedStory] = useState<
28 | IStoryIndexedObject | undefined
29 | >();
30 | const firstStoryIndex = 0;
31 | const lastStoryIndex = stories.length - 1;
32 | const [isPaused, setIsPaused] = useState(false);
33 |
34 | const hasCalledEndedCb = useRef(false);
35 | const hasCalledStartedCb = useRef(false);
36 |
37 | useEffect(() => {
38 | if (!hasCalledStartedCb.current) {
39 | hasCalledStartedCb.current = true;
40 | onStoriesStart();
41 | }
42 | }, [onStoriesStart]);
43 |
44 | useEffect(() => {
45 | const story = storiesWithIndex[currentIndex];
46 | if (story) {
47 | setSelectedStory(story);
48 | }
49 | }, [currentIndex, stories]);
50 |
51 | function handleNextClick() {
52 | if (loop && selectedStory?.index === lastStoryIndex) {
53 | setSelectedStory(storiesWithIndex[firstStoryIndex]);
54 | return;
55 | }
56 | if (!hasCalledEndedCb.current && selectedStory?.index === lastStoryIndex) {
57 | onAllStoriesEnd();
58 | hasCalledEndedCb.current = true;
59 | }
60 | if (selectedStory?.index === lastStoryIndex) {
61 | return;
62 | }
63 | setSelectedStory((prev) => {
64 | if (!prev) {
65 | return storiesWithIndex[0];
66 | }
67 | const newIndex = prev?.index + 1;
68 | return storiesWithIndex[newIndex];
69 | });
70 | }
71 | function handlePrevClick() {
72 | if (selectedStory?.index === firstStoryIndex) {
73 | return;
74 | }
75 | setSelectedStory((prev) => {
76 | if (!prev) {
77 | return storiesWithIndex[0];
78 | }
79 | const newIndex = prev?.index - 1;
80 | return storiesWithIndex[newIndex];
81 | });
82 | }
83 |
84 | function handlePause() {
85 | setIsPaused(true);
86 | }
87 | function handleResume() {
88 | setIsPaused(false);
89 | }
90 |
91 | useEffect(() => {
92 | if (selectedStory) {
93 | onStoryChange(selectedStory.index);
94 | }
95 | }, [selectedStory]);
96 |
97 | hooks.usePausableTimeout(
98 | () => {
99 | handleNextClick();
100 | },
101 | selectedStory?.calculatedDuration ?? null,
102 | isPaused,
103 | );
104 |
105 | hooks.useWindowVisibility((isWindowInFocus) => {
106 | if (pauseStoryWhenInActiveWindow) {
107 | setIsPaused(!isWindowInFocus);
108 | }
109 | });
110 |
111 | const contextValue: IStoryContext = {
112 | stories: storiesWithIndex,
113 | width,
114 | height,
115 | defaultDuration,
116 | isPaused,
117 | classNames,
118 | };
119 |
120 | if (!selectedStory) {
121 | return null;
122 | }
123 | return (
124 |
125 |
144 |
145 | );
146 | }
147 |
--------------------------------------------------------------------------------
/packages/stories/src/styles.css:
--------------------------------------------------------------------------------
1 | .main {
2 | position: relative;
3 | touch-action: manipulation;
4 | background-color: black;
5 | user-select: none;
6 | -webkit-touch-callout:none;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/stories/src/types.d.ts:
--------------------------------------------------------------------------------
1 | interface IStoryObject {
2 | type: string;
3 | url: string;
4 | duration: number;
5 | component?: any;
6 | header?: any;
7 | seeMore?: any;
8 | seeMoreComponent?: any;
9 | onSeeMoreClick?: (storyIndex: number) => void;
10 | }
11 |
12 | interface IStoryIndexedObject extends IStoryObject {
13 | index: number;
14 | calculatedDuration: number;
15 | }
16 |
17 | interface IStoryClassNames {
18 | main?: string;
19 | progressContainer?: string;
20 | progressBarContainer?: string;
21 | progressBar?: string;
22 | storyContainer?: string;
23 | }
24 |
25 | export interface IStoryProps {
26 | stories: IStoryObject[];
27 | height?: '100%';
28 | width?: '100%';
29 | onStoryChange: (currentIndex: number) => void;
30 | currentIndex?: number;
31 | defaultDuration?: number;
32 | loop?: boolean;
33 | onStoriesStart?: () => void;
34 | onAllStoriesEnd?: () => void;
35 | classNames?: IStoryClassNames;
36 | pauseStoryWhenInActiveWindow?: boolean;
37 | }
38 |
39 | export interface IStoryContext {
40 | stories: IStoryIndexedObject[];
41 | height?: '100%';
42 | width?: '100%';
43 | defaultDuration: number;
44 | isPaused: boolean;
45 | classNames?: IStoryClassNames;
46 | }
47 |
48 | export interface IStoryComponentProps {
49 | story: IStoryIndexedObject;
50 | onPause: () => void;
51 | onResume: () => void;
52 | isPaused: boolean;
53 | }
54 |
--------------------------------------------------------------------------------
/packages/stories/src/utilities.ts:
--------------------------------------------------------------------------------
1 | import { IStoryObject, IStoryIndexedObject } from './types';
2 |
3 | function getTimeDelta(precesion = 4): number {
4 | return Number(Math.random().toFixed(precesion));
5 | }
6 |
7 | export function transformStories(
8 | stories: IStoryObject[],
9 | defaultDuration: number,
10 | ): IStoryIndexedObject[] {
11 | /*
12 | * adding some delta time to duration to have distinct duration for each story.
13 | * this is required inside the timeout hook.
14 | * otherwise the effect is not getting called which resets the delay
15 | * after each story
16 | */
17 |
18 | let lastCalculatedDuration = 0;
19 | return stories.map((story, index) => {
20 | const duration = story.duration || defaultDuration;
21 | let calculatedDuration = duration + getTimeDelta();
22 | /*
23 | * it is possible that there is a collision in delta time generated.
24 | * in that case we are storing last calculatedDuration and comparing it
25 | * on each iteration with next calculated duration.
26 | * if collision occured and we got same duration we are re generating the time delta
27 | * with a different precision. slightly higer then default
28 | * */
29 | if (calculatedDuration === lastCalculatedDuration) {
30 | calculatedDuration = duration + getTimeDelta(6);
31 | }
32 |
33 | lastCalculatedDuration = calculatedDuration;
34 | return {
35 | ...story,
36 | index,
37 | calculatedDuration,
38 | };
39 | });
40 | }
41 |
--------------------------------------------------------------------------------
/packages/stories/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "strict": true,
5 | "target": "es5",
6 | "module": "esnext",
7 | "jsx": "react-jsx",
8 | "outDir": "./dist",
9 | "declaration": true,
10 | "declarationDir": "./dist",
11 | "declarationMap": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "importHelpers": true,
14 | "moduleResolution": "Node"
15 | },
16 | "exclude": ["node_modules", "build", "dist", "example", "rollup.config.js"],
17 | "useTsconfigDeclarationDir": true
18 | }
19 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | const PrettierConfig = {
2 | printWidth: 80,
3 | singleQuote: true,
4 | trailingComma: 'all',
5 | arrowParens: 'always',
6 | };
7 |
8 | module.exports = PrettierConfig;
9 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | stories-react
2 | A React component For Instagram like stories
3 |
4 | Homepage
5 |
6 |
11 |
22 |
30 |
38 |
39 |
40 |
41 |
42 |
43 | # Install
44 |
45 | ```sh
46 | npm install --save stories-react
47 | ```
48 |
49 | # Demo
50 |
51 | You can find working demo [here](https://hannadrehman.github.io/stories-react/)
52 |
53 | # Usage
54 |
55 | ```jsx
56 | import React from 'react';
57 | import Stories from 'stories-react';
58 | import 'stories-react/dist/index.css';
59 |
60 | export default function ImagesStories() {
61 | const stories = [
62 | {
63 | type: 'image',
64 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
65 | duration: 5000,
66 | },
67 | {
68 | type: 'image',
69 | duration: 6000,
70 | url: 'https://images.pexels.com/photos/9733197/pexels-photo-9733197.jpeg?w=300',
71 | },
72 | {
73 | duration: 7000,
74 | type: 'image',
75 | url: 'https://images.pexels.com/photos/9470805/pexels-photo-9470805.jpeg?w=300',
76 | },
77 | ];
78 |
79 | return ;
80 | }
81 | ```
82 |
83 | ## Props
84 |
85 | | Property | Type | Default | Description |
86 | | ------------------------------ | ------------------------ | ------- | ----------------------------------------------------------------------------------- |
87 | | `stories` | `IStoryObject[]` | `[]` | An array of story objects. description of `IStoryObject` is mentioned below |
88 | | `height` | `string` | `100%` | Height of story container |
89 | | `width` | `string` | `100%` | Width of story container |
90 | | `onStoryChange` | `function(index:number)` | `-` | Callback called when story changes |
91 | | `onStoriesStart` | `function` | `-` | Callback called when first story is rendered. it get called only once, |
92 | | `onAllStoriesEnd` | `function` | `-` | Callback called when last story gets completed. it will get called only once |
93 | | `currentIndex` | `number` | `-` | Current index of the story which should be selected first |
94 | | `defaultDuration` | `number` | `10000` | default duration in ms of stories if duration is not provided in the `IStoryObject` |
95 | | `classNames` | `IStoryClassNames` | `{}` | classnames to overide different sections of a story renderer |
96 | | `pauseStoryWhenInActiveWindow` | `boolean` | `true` | pauses story when window goes out of focus (user changes tab/minimizes browser etc |
97 |
98 | ## IStoryObject
99 |
100 | | Property | Type | Default | Description |
101 | | ------------------ | ------------------------------------ | ------- | ------------------------------------------------ |
102 | | `type` | `image , video , component` | `-` | type of story to render |
103 | | `url` | `string` | `-` | url of a story (image & video only) |
104 | | `duration` | `duration` | `10000` | duration in ms of the story |
105 | | `component` | `React Component` | `-` | custom component to render as a story |
106 | | `header` | `React Component` | `-` | header of a story |
107 | | `seeMore` | `string , boolean , React Component` | `true` | See more action text |
108 | | `seeMoreComponent` | `React Component` | `-` | see more component opens after clicking see more |
109 | | `onSeeMoreClick` | `function(index:number)` | `-` | Callback called when story see more is clicked |
110 |
111 | ## IStoryClassNames
112 |
113 | | Property | Type | Default | Description |
114 | | ---------------------- | -------- | ------- | ----------------------------------------------- |
115 | | `main` | `string` | `-` | classname for main container |
116 | | `progressContainer` | `string` | `-` | classname for progress line container |
117 | | `progressBarContainer` | `string` | `-` | classname for single progress bar box container |
118 | | `progressBar` | `string` | `-` | classname for progress bar |
119 | | `storyContainer` | `string` | `-` | classname for story container |
120 |
121 | ## Custom Component Story Props
122 |
123 | | Property | Type | | Description |
124 | | ---------- | -------------- | --- | ------------------------- |
125 | | `pause` | `function` | | call it to pause a story |
126 | | `resume` | `function` | | call it to resume a story |
127 | | `story` | `IStoryObject` | | current story properties |
128 | | `isPaused` | `boolean` | | state of a story |
129 |
--------------------------------------------------------------------------------