├── .eslintignore ├── .storybook ├── addons.js ├── preview-head.html ├── config.js └── webpack.config.js ├── tests ├── __mocks__ │ └── fileMock.js └── Storyshots.test.js ├── .editorconfig ├── .npmignore ├── .babelrc ├── timeline.png ├── docs ├── favicon.ico ├── ef2dd493e0f7c46d86759d30247aae16.jpg ├── iframe.html └── index.html ├── jetbrains-badge.png ├── stories ├── sample.jpg └── App.story.js ├── .gitignore ├── components ├── index.js ├── Timeline.js ├── TimelineBlip.js ├── styles.js └── TimelineEvent.js ├── .eslintrc ├── .travis.yml ├── jest.config.js ├── typings ├── tsconfig.json ├── index.d.ts └── index-tests.tsx ├── LICENSE.md ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | coverage/ 3 | lib/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-options/register' 2 | -------------------------------------------------------------------------------- /tests/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub' 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | indent_style = space 3 | indent_size = 2 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | src 4 | test 5 | examples 6 | coverage 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcdexta/react-event-timeline/HEAD/timeline.png -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcdexta/react-event-timeline/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /jetbrains-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcdexta/react-event-timeline/HEAD/jetbrains-badge.png -------------------------------------------------------------------------------- /stories/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcdexta/react-event-timeline/HEAD/stories/sample.jpg -------------------------------------------------------------------------------- /docs/ef2dd493e0f7c46d86759d30247aae16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcdexta/react-event-timeline/HEAD/docs/ef2dd493e0f7c46d86759d30247aae16.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | dist 5 | lib 6 | coverage 7 | .idea 8 | examples/bundle.js 9 | examples/style.css 10 | npm-debug.log 11 | -------------------------------------------------------------------------------- /tests/Storyshots.test.js: -------------------------------------------------------------------------------- 1 | const initStoryshots = require('@storybook/addon-storyshots').default 2 | initStoryshots({ 3 | storyNameRegex: /^((?!.*?DontTest).)*$/ 4 | }) 5 | -------------------------------------------------------------------------------- /components/index.js: -------------------------------------------------------------------------------- 1 | import Timeline from './Timeline' 2 | import TimelineEvent from './TimelineEvent' 3 | import TimelineBlip from './TimelineBlip' 4 | 5 | export { Timeline, TimelineEvent, TimelineBlip } 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["standard", "standard-react"], 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "space-before-function-paren": 0, 10 | "react/no-did-update-set-state": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - ~/.npm 5 | notifications: 6 | email: true 7 | node_js: 8 | - '8' 9 | before_script: 10 | - npm prune 11 | after_success: 12 | - npm run coverage 13 | - npm run semantic-release 14 | branches: 15 | except: 16 | - /^v\d+\.\d+\.\d+$/ 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | transform: { 6 | '^.+\\.jsx?$': 'babel-jest' 7 | }, 8 | 'moduleNameMapper': { 9 | '\\.(jpg|png|gif|svg)$': '/tests/__mocks__/fileMock.js' 10 | }, 11 | 'coveragePathIgnorePatterns': [ 12 | '/stories/', 13 | '/.storybook/', 14 | '/node_modules/', 15 | 'story(.*).tsx' 16 | ], 17 | collectCoverage: true 18 | } 19 | -------------------------------------------------------------------------------- /typings/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "forceConsistentCasingInFileNames": true, 5 | "jsx": "react", 6 | "lib": [ 7 | "es6", 8 | "dom" 9 | ], 10 | "module": "commonjs", 11 | "noEmit": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noImplicitAny": true, 15 | "noImplicitThis": true, 16 | "strictNullChecks": true, 17 | "typeRoots": [ 18 | "../" 19 | ], 20 | "types": [] 21 | }, 22 | "files": [ 23 | "index.d.ts", 24 | "index-tests.tsx" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { addDecorator, configure } from '@storybook/react' 2 | import { withOptions } from '@storybook/addon-options' 3 | import { withInfo } from '@storybook/addon-info' 4 | 5 | addDecorator( 6 | withOptions({ 7 | name: 'react event timeline', 8 | url: 'https://github.com/rcdexta/react-event-timeline', 9 | goFullScreen: false, 10 | showLeftPanel: true, 11 | showDownPanel: false, 12 | showSearchBox: false, 13 | downPanelInRight: false 14 | }) 15 | ) 16 | 17 | addDecorator( 18 | withInfo({ 19 | header: false, 20 | inline: true, 21 | source: true, 22 | propTables: false 23 | }) 24 | ) 25 | 26 | function loadStories() { 27 | require('../stories/App.story') 28 | } 29 | 30 | configure(loadStories, module) 31 | -------------------------------------------------------------------------------- /docs/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | Storybook 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | // you can use this file to add your custom webpack plugins, loaders and anything you like. 3 | // This is just the basic way to add additional webpack configurations. 4 | // For more information refer the docs: https://storybook.js.org/configurations/custom-webpack-config 5 | 6 | // IMPORTANT 7 | // When you add this file, we won't add the default configurations which is similar 8 | // to "React Create lib". This only has babel loader to load JavaScript. 9 | 10 | const path = require('path'); 11 | 12 | module.exports = { 13 | plugins: [ 14 | // your custom plugins 15 | ], 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.scss$/, 20 | loaders: ["style", "css", "sass"], 21 | include: path.resolve(__dirname, '../') 22 | }, 23 | { 24 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, 25 | loader: "url-loader?limit=10000&mimetype=application/font-woff" 26 | }, 27 | { 28 | test: /\.(jpg|ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 29 | loader: "file-loader" 30 | } 31 | ], 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 RC 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 | -------------------------------------------------------------------------------- /components/Timeline.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import s from './styles' 4 | 5 | class Timeline extends Component { 6 | render () { 7 | const { orientation = 'left', children, lineColor, lineStyle, style, ...otherProps } = this.props 8 | const childrenWithProps = React.Children.map(children, child => React.cloneElement(child, { orientation })) 9 | let leftOrRight = (orientation === 'right') ? { ...s['containerBefore--right'] } : { ...s['containerBefore--left'] } 10 | let lineAppearance = { ...leftOrRight, ...lineStyle } 11 | lineAppearance = lineColor ? { ...lineAppearance, background: lineColor } : lineAppearance 12 | return ( 13 |
14 |
15 |
16 | {childrenWithProps} 17 |
18 |
19 |
20 | ) 21 | } 22 | } 23 | 24 | Timeline.propTypes = { 25 | children: PropTypes.node.isRequired, 26 | orientation: PropTypes.string, 27 | style: PropTypes.object, 28 | lineColor: PropTypes.string, 29 | lineStyle: PropTypes.object 30 | } 31 | 32 | Timeline.defaultProps = { 33 | style: {}, 34 | lineStyle: {} 35 | } 36 | 37 | export default Timeline 38 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface TimelineProps { 4 | children: React.ReactNode; 5 | orientation?: string; 6 | style?: React.CSSProperties; 7 | lineColor?: string; 8 | lineStyle?: object; 9 | } 10 | 11 | export interface TimelineEventProps { 12 | title: React.ReactNode; 13 | subtitle?: React.ReactNode; 14 | createdAt?: React.ReactNode; 15 | children?: React.ReactNode; 16 | buttons?: React.ReactNode; 17 | container?: string; 18 | icon?: React.ReactNode; 19 | iconColor?: string; 20 | iconStyle?: object; 21 | bubbleStyle?: object; 22 | orientation?: string; 23 | contentStyle?: object; 24 | cardHeaderStyle?: object; 25 | style?: object; 26 | titleStyle?: object; 27 | subtitleStyle?: object; 28 | collapsible?: boolean; 29 | showContent?: boolean; 30 | className?: string; 31 | onClick?: (evt: React.MouseEvent) => void 32 | onIconClick?: (evt: React.MouseEvent) => void 33 | } 34 | 35 | export interface TimelineBlipProps { 36 | title: React.ReactNode; 37 | icon?: React.ReactNode; 38 | iconColor?: string; 39 | iconStyle?: object; 40 | style?: object; 41 | orientation?: string; 42 | } 43 | 44 | export class Timeline extends React.Component {} 45 | export class TimelineEvent extends React.Component {} 46 | export class TimelineBlip extends React.Component {} 47 | -------------------------------------------------------------------------------- /components/TimelineBlip.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import s from './styles' 4 | 5 | class TimelineBlip extends Component { 6 | mergeNotificationStyle(iconColor) { 7 | return iconColor ? { ...s.eventType, ...{ color: iconColor, borderColor: iconColor } } : s.eventType 8 | } 9 | 10 | iconStyle(iconStyle) { 11 | return { ...s.materialIcons, ...iconStyle } 12 | } 13 | 14 | render() { 15 | const { title, iconStyle, icon, orientation, iconColor, style, ...otherProps } = this.props 16 | const leftOrRightEvent = (orientation === 'right') ? { ...s['event--right'] } : { ...s['event--left'] } 17 | return ( 18 |
19 |
20 | {icon} 21 |
22 |
23 |
{title}
24 |
25 |
26 |
27 | ) 28 | } 29 | } 30 | 31 | TimelineBlip.propTypes = { 32 | title: PropTypes.node.isRequired, 33 | icon: PropTypes.node, 34 | iconColor: PropTypes.string, 35 | iconStyle: PropTypes.object, 36 | style: PropTypes.object, 37 | orientation: PropTypes.string 38 | } 39 | 40 | TimelineBlip.defaultProps = { 41 | iconStyle: {}, 42 | style: {} 43 | } 44 | 45 | export default TimelineBlip 46 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Storybook 10 | 39 | 40 | 41 | 42 |
43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /typings/index-tests.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Timeline, TimelineEvent, TimelineBlip } from "."; 3 | 4 | export const UseTimelineBlip = ( 5 | 6 | assignment_late} 10 | iconColor="#03a9f4" 11 | style={{ 12 | color: "#9c27b0" 13 | }} 14 | /> 15 | grade} 18 | iconColor="#6fba1c" 19 | /> 20 | 21 | ); 22 | 23 | export default ( 24 | 25 | textsms} 29 | > 30 | I received the payment for $543. Should be shipping the item within a 31 | couple of hours. 32 | 33 | email} 37 | > 38 | Like we talked, you said that you would share the shipment details? This 39 | is an urgent order and so I am losing patience. Can you expedite the 40 | process and pls do share the details asap. Consider this a gentle reminder 41 | if you are on track already! 42 | 43 | 44 | ); 45 | -------------------------------------------------------------------------------- /components/styles.js: -------------------------------------------------------------------------------- 1 | let style = { 2 | 3 | container: { 4 | position: 'relative', 5 | fontSize: '80%', 6 | fontWeight: 300, 7 | padding: '10px 0', 8 | width: '95%', 9 | margin: '0 auto' 10 | }, 11 | containerBefore: { 12 | content: '', 13 | position: 'absolute', 14 | top: 0, 15 | height: '100%', 16 | width: 2, 17 | background: '#a0b2b8' 18 | }, 19 | 'containerBefore--left': { 20 | left: '16px' 21 | }, 22 | 'containerBefore--right': { 23 | right: '14px' 24 | }, 25 | containerAfter: { 26 | content: '', 27 | display: 'table', 28 | clear: 'both' 29 | }, 30 | event: { 31 | position: 'relative', 32 | margin: '10px 0' 33 | }, 34 | 'event--left': { 35 | paddingLeft: 45, 36 | textAlign: 'left' 37 | }, 38 | 'event--right': { 39 | paddingRight: 45, 40 | textAlign: 'right' 41 | }, 42 | eventAfter: { 43 | clear: 'both', 44 | content: '', 45 | display: 'table' 46 | }, 47 | eventType: { 48 | position: 'absolute', 49 | top: 0, 50 | borderRadius: '50%', 51 | width: 30, 52 | height: 30, 53 | marginLeft: 1, 54 | background: '#e9f0f5', 55 | border: '2px solid #6fba1c', 56 | display: 'flex' 57 | }, 58 | 'eventType--left': { 59 | left: 0 60 | }, 61 | 'eventType--right': { 62 | right: 0 63 | }, 64 | materialIcons: { 65 | display: 'flex', 66 | width: 32, 67 | height: 32, 68 | position: 'relative', 69 | justifyContent: 'center', 70 | cursor: 'pointer', 71 | alignSelf: 'center', 72 | alignItems: 'center' 73 | }, 74 | eventContainer: { 75 | position: 'relative' 76 | }, 77 | eventContainerBefore: { 78 | top: 24, 79 | left: '100%', 80 | borderColor: 'transparent', 81 | borderLeftColor: '#ffffff' 82 | }, 83 | time: { 84 | marginBottom: 3 85 | }, 86 | subtitle: { 87 | marginTop: 2, 88 | fontSize: '85%', 89 | color: '#777' 90 | }, 91 | message: { 92 | width: '98%', 93 | backgroundColor: '#ffffff', 94 | boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1)', 95 | marginTop: '1em', 96 | marginBottom: '1em', 97 | lineHeight: 1.6, 98 | padding: '0.5em 1em' 99 | }, 100 | messageAfter: { 101 | clear: 'both', 102 | content: '', 103 | display: 'table' 104 | }, 105 | actionButtons: { 106 | marginTop: -20 107 | }, 108 | 'actionButtons--left': { 109 | float: 'left', 110 | textAlign: 'left' 111 | }, 112 | 'actionButtons--right': { 113 | float: 'right', 114 | textAlign: 'right' 115 | }, 116 | card: { 117 | boxShadow: 'rgba(0, 0, 0, 0.117647) 0px 1px 6px, rgba(0, 0, 0, 0.117647) 0px 1px 4px', 118 | backgroundColor: 'rgb(255, 255, 255)' 119 | }, 120 | cardTitle: { 121 | backgroundColor: '#7BB1EA', 122 | padding: 10, 123 | color: '#fff' 124 | }, 125 | cardBody: { 126 | backgroundColor: '#ffffff', 127 | marginBottom: '1em', 128 | lineHeight: 1.6, 129 | padding: 10, 130 | minHeight: 40 131 | }, 132 | blipStyle: { 133 | position: 'absolute', 134 | top: '50%', 135 | marginTop: '9px' 136 | }, 137 | toggleEnabled: { 138 | cursor: 'pointer' 139 | } 140 | } 141 | 142 | export default style 143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-event-timeline", 3 | "description": "A responsive event timeline in React.js", 4 | "main": "dist/index.js", 5 | "typings": "typings", 6 | "files": [ 7 | "dist", 8 | "typings", 9 | "README" 10 | ], 11 | "scripts": { 12 | "coverage": "codecov -f ./coverage/lcov.info", 13 | "lint": "eslint components/ stories/ tests", 14 | "lintfix": "npm run lint -- --fix", 15 | "prepublish": "npm run lint && npm run build", 16 | "storybook": "start-storybook -p 9002", 17 | "test": "jest && tsc -p typings", 18 | "test:watch": "jest --watch", 19 | "build": "babel components --out-dir dist", 20 | "docs": "build-storybook -o docs", 21 | "commit": "git cz", 22 | "deploy-storybook": "storybook-to-ghpages", 23 | "travis-deploy-once": "travis-deploy-once", 24 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/rcdexta/react-event-timeline.git" 29 | }, 30 | "keywords": [ 31 | "react", 32 | "timeline", 33 | "notification" 34 | ], 35 | "author": "RC", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/rcdexta/react-event-timeline/issues" 39 | }, 40 | "homepage": "https://github.com/rcdexta/react-event-timeline", 41 | "devDependencies": { 42 | "@babel/cli": "^7.1.5", 43 | "@babel/core": "^7.1.6", 44 | "@babel/preset-env": "^7.1.6", 45 | "@babel/preset-react": "^7.0.0", 46 | "@storybook/addon-info": "^4.0.11", 47 | "@storybook/addon-options": "^5.1.9", 48 | "@storybook/addon-storyshots": "^4.0.11", 49 | "@storybook/addons": "^4.0.11", 50 | "@storybook/react": "^4.0.9", 51 | "@storybook/storybook-deployer": "^2.5.2", 52 | "@types/react": "^16.7.11", 53 | "autoprefixer": "^9.4.0", 54 | "babel-core": "^7.0.0-bridge.0", 55 | "babel-eslint": "^10.0.1", 56 | "babel-jest": "^24.8.0", 57 | "babel-loader": "^8.0.4", 58 | "codecov": "^3.1.0", 59 | "commitizen": "^3.0.5", 60 | "cz-conventional-changelog": "^2.1.0", 61 | "eslint": "^5.9.0", 62 | "eslint-config-standard": "^12.0.0", 63 | "eslint-config-standard-react": "^7.0.2", 64 | "eslint-plugin-import": "^2.14.0", 65 | "eslint-plugin-node": "^8.0.0", 66 | "eslint-plugin-promise": "^4.0.1", 67 | "eslint-plugin-react": "^7.11.1", 68 | "eslint-plugin-standard": "^4.0.0", 69 | "eventsource-polyfill": "^0.9.6", 70 | "file-loader": "^2.0.0", 71 | "husky": "^1.2.0", 72 | "jest": "^23.6.0", 73 | "lint-staged": "^8.1.0", 74 | "node-sass": "^4.10.0", 75 | "react": "^16.6.3", 76 | "react-dom": "^16.6.3", 77 | "react-test-renderer": "^16.6.3", 78 | "rimraf": "^2.5.4", 79 | "sass-loader": "^7.1.0", 80 | "semantic-release": "^15.13.16", 81 | "travis-deploy-once": "^5.0.9", 82 | "typescript": "^3.2.1" 83 | }, 84 | "dependencies": { 85 | "prop-types": "^15.6.0" 86 | }, 87 | "peerDependencies": { 88 | "react": ">= 0.14.0 < 17.0.0-0" 89 | }, 90 | "config": { 91 | "commitizen": { 92 | "path": "node_modules/cz-conventional-changelog" 93 | } 94 | }, 95 | "version": "0.0.0-development", 96 | "lint-staged": { 97 | "*.{js,jsx}": [ 98 | "eslint --fix", 99 | "git add" 100 | ] 101 | }, 102 | "husky": { 103 | "hooks": { 104 | "pre-commit": "lint-staged" 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /components/TimelineEvent.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import React, { Component } from 'react' 3 | 4 | import s from './styles' 5 | 6 | class TimelineEvent extends Component { 7 | constructor(props) { 8 | super(props) 9 | this.state = { showContent: this.props.showContent } 10 | this.toggleContent = this.toggleContent.bind(this) 11 | } 12 | 13 | componentDidUpdate(prevProps) { 14 | if (this.props.showContent !== prevProps.showContent) { 15 | this.setState({ showContent: this.props.showContent }) 16 | } 17 | } 18 | 19 | mergeNotificationStyle(iconColor, bubbleStyle, orientation) { 20 | const iconColorStyle = iconColor 21 | ? { ...s.eventType, ...{ color: iconColor, borderColor: iconColor } } 22 | : s.eventType 23 | const leftOrRight = 24 | orientation === 'right' 25 | ? { ...s['eventType--right'] } 26 | : { ...s['eventType--left'] } 27 | return { ...iconColorStyle, ...leftOrRight, ...bubbleStyle } 28 | } 29 | 30 | mergeContentStyle(contentStyle) { 31 | const messageStyle = this.showAsCard() ? s.cardBody : s.message 32 | return contentStyle ? { ...messageStyle, ...contentStyle } : messageStyle 33 | } 34 | 35 | timeStyle() { 36 | return this.showAsCard() ? s.time : { ...s.time, color: '#303e49' } 37 | } 38 | 39 | showAsCard() { 40 | const { container } = this.props 41 | return container === 'card' 42 | } 43 | 44 | containerStyle() { 45 | const { style } = this.props 46 | const containerStyle = { ...s.eventContainer, ...style } 47 | return this.showAsCard() ? { ...s.card, ...containerStyle } : containerStyle 48 | } 49 | 50 | toggleStyle() { 51 | const { container, cardHeaderStyle, collapsible } = this.props 52 | const messageStyle = 53 | container === 'card' ? { ...s.cardTitle, ...cardHeaderStyle } : {} 54 | return collapsible ? { ...s.toggleEnabled, ...messageStyle } : messageStyle 55 | } 56 | 57 | toggleContent() { 58 | this.setState({ showContent: !this.state.showContent }) 59 | } 60 | 61 | renderChildren() { 62 | const { collapsible, contentStyle } = this.props 63 | return (collapsible && this.state.showContent) || !collapsible ? ( 64 |
65 | {this.props.children} 66 |
67 |
68 | ) : ( 69 | 73 | … 74 | 75 | ) 76 | } 77 | 78 | render() { 79 | const { 80 | createdAt, 81 | title, 82 | subtitle, 83 | iconStyle, 84 | bubbleStyle, 85 | buttons, 86 | icon, 87 | iconColor, 88 | titleStyle, 89 | subtitleStyle, 90 | orientation, 91 | collapsible, 92 | onClick, 93 | onIconClick, 94 | className 95 | } = this.props 96 | const leftOrRightEventStyling = 97 | orientation === 'right' 98 | ? { ...s['event--right'] } 99 | : { ...s['event--left'] } 100 | const leftOrRightButtonStyling = 101 | orientation === 'left' 102 | ? { ...s['actionButtons--right'] } 103 | : { ...s['actionButtons--left'] } 104 | return ( 105 |
106 |
113 | 117 | {icon} 118 | 119 |
120 |
121 |
122 |
126 | {createdAt &&
{createdAt}
} 127 |
{title}
128 | {subtitle && ( 129 |
{subtitle}
130 | )} 131 |
132 | {buttons} 133 |
134 |
135 | {this.props.children && this.renderChildren()} 136 |
137 |
138 |
139 | ) 140 | } 141 | } 142 | 143 | TimelineEvent.propTypes = { 144 | title: PropTypes.node.isRequired, 145 | subtitle: PropTypes.node, 146 | createdAt: PropTypes.node, 147 | children: PropTypes.node, 148 | buttons: PropTypes.node, 149 | container: PropTypes.string, 150 | icon: PropTypes.node, 151 | iconColor: PropTypes.string, 152 | iconStyle: PropTypes.object, 153 | bubbleStyle: PropTypes.object, 154 | orientation: PropTypes.string, 155 | contentStyle: PropTypes.object, 156 | cardHeaderStyle: PropTypes.object, 157 | style: PropTypes.object, 158 | titleStyle: PropTypes.object, 159 | subtitleStyle: PropTypes.object, 160 | collapsible: PropTypes.bool, 161 | showContent: PropTypes.bool, 162 | className: PropTypes.string, 163 | onClick: PropTypes.func, 164 | onIconClick: PropTypes.func 165 | } 166 | 167 | TimelineEvent.defaultProps = { 168 | createdAt: undefined, 169 | iconStyle: {}, 170 | bubbleStyle: {}, 171 | contentStyle: {}, 172 | cardHeaderStyle: {}, 173 | style: {}, 174 | titleStyle: {}, 175 | subtitleStyle: {}, 176 | orientation: 'left', 177 | showContent: false, 178 | className: '', 179 | onClick: () => {}, 180 | onIconClick: () => {} 181 | } 182 | 183 | export default TimelineEvent 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-event-timeline 2 | 3 | React component to generate a responsive vertical event timeline 4 | 5 | [![npm version](https://badge.fury.io/js/react-event-timeline.svg)](https://badge.fury.io/js/react-event-timeline) 6 | [![Build Status](https://travis-ci.org/rcdexta/react-event-timeline.svg?branch=master)](https://travis-ci.org/rcdexta/react-event-timeline) 7 | [![codecov](https://codecov.io/gh/rcdexta/react-event-timeline/branch/master/graph/badge.svg)](https://codecov.io/gh/rcdexta/react-event-timeline) 8 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/rcdexta/react-event-timeline/blob/master/LICENSE.md) 9 | 10 | ![alt tag](https://github.com/rcdexta/react-event-timeline/raw/master/timeline.png) 11 | 12 | Storybook demos here: https://rcdexta.github.io/react-event-timeline 13 | 14 | CodeSandbox version to play with examples (in typescript): 15 | 16 | [![Edit Timeline Example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/13k1jjqj64) 17 | 18 | > Note: CodeSandbox version has predefined styles and icons loaded in index.html for better presentation! 19 | 20 | ## Features 21 | 22 | * Is lightweight 23 | * Responsive and extensible 24 | * Configurable and customizable 25 | 26 | ## Getting started 27 | 28 | To install as npm dependency 29 | ``` 30 | npm install --save react-event-timeline 31 | ``` 32 | or if you use `yarn` 33 | ``` 34 | yarn add react-event-timeline 35 | ``` 36 | 37 | ## Usage 38 | 39 | The following snippet generates the timeline you see in the screenshot: 40 | 41 | ```jsx 42 | import {Timeline, TimelineEvent} from 'react-event-timeline' 43 | 44 | ReactDOM.render( 45 | 46 | textsms} 49 | > 50 | I received the payment for $543. Should be shipping the item within a couple of hours. 51 | 52 | email} 56 | > 57 | Like we talked, you said that you would share the shipment details? This is an urgent order and so I 58 | am losing patience. Can you expedite the process and pls do share the details asap. Consider this a 59 | gentle reminder if you are on track already! 60 | 61 | , 62 | document.getElementById('container') 63 | ); 64 | ``` 65 | 66 | Please refer [storybook](https://github.com/rcdexta/react-event-timeline/blob/master/stories/App.story.js) to check code for all the examples in the storybook demo. 67 | 68 | ## API Documentation 69 | 70 | ### Timeline 71 | 72 | This is the wrapper component that creates the infinite vertical timeline 73 | 74 | | Name | Type | Description | 75 | | -------------- | ------ | ---------------------------------------- | 76 | | className | string | The css class name of timeline container| 77 | | style | object | Override inline styles of timeline container | 78 | | orientation | string | Display the timeline on `right` or `left`. Default: `left` | 79 | | lineColor | string | CSS color code to override the line color | 80 | | lineStyle | string | Override the appearance of line with custom css styling | 81 | 82 | ### TimelineEvent 83 | 84 | Each event in the timeline will be represented by the `TimelineEvent` component. There can be multiple repeating instances of this component inside `Timeline` wrapper 85 | 86 | | Name | Type | Description | 87 | | ------------ | ------ | ---------------------------------------- | 88 | | title | node | The title of the event. Can be string or any DOM element node(s) | 89 | | createdAt | node | The time at which the event occurred. Can be datetime string or any DOM element node(s) | 90 | | subtitle | node | If you prefer having the title at the top and some caption below, omit createdAt and specify title and subtitle | 91 | | icon | node | The icon to show as event lable. Can be a SVG or font icon | 92 | | iconStyle | object | Custom CSS styling for the icon | 93 | | bubbleStyle | object | Custom CSS styling for the bubble containing the icon | 94 | | buttons | node | Action buttons to display to the right of the event content | 95 | | contentStyle | node | Override content style | 96 | | container | string | Optional value `card` will render event as a Card | 97 | | style | object | Override style for the entire event container. Can be used to modify card appearance if container is selected as `card` | 98 | | titleStyle | object | Override style for the title content | 99 | | subtitleStyle | object | Override style for the subtitle content | 100 | | cardHeaderStyle | object | Override style for the card header if container is `card` | 101 | | collapsible | boolean | Make the timeline event collapse body content | 102 | | showContent | boolean | if `collapsible` is true, should content be shown by default. `false` is default value | 103 | 104 | ### TimelineBlip 105 | 106 | Use this component if your event footprint is too small and can be described in a single line 107 | 108 | | Name | Type | Description | 109 | | --------- | ------ | ---------------------------------------- | 110 | | title | node | The title of the event. Can be string or any DOM element node(s) | 111 | | icon | node | The icon to show as event label. Can be a SVG or font icon | 112 | | iconColor | string | CSS color code for icon | 113 | | iconStyle | object | Custom CSS styling for the icon | 114 | | style | object | Override style for the entire event container | 115 | 116 | Refer to Condensed Timeline in Storybook for examples of using this component. 117 | 118 | ## Development 119 | 120 | This project recommends using [react-storybook](https://github.com/kadirahq/react-storybook) as a UI component development environment. Use the following scripts for your development workflow: 121 | 122 | 1. `npm run storybook`: Start developing by using storybook 123 | 2. `npm run lint` : Lint all js files 124 | 3. `npm run lintfix` : fix linting errors of all js files 125 | 4. `npm run build`: transpile all ES6 component files into ES5(commonjs) and put it in `dist` directory 126 | 5. `npm run docs`: create static build of storybook in `docs` directory that can be used for github pages 127 | 128 | The storybook artefacts can be found in `stories` folder. Run `npm run storybook` and you should see your code changes hot reloaded on the browser 129 | 130 | Also use [semantic-release](https://github.com/semantic-release/semantic-release) to automate release to npm. Use `npm run commit` to commit your changes and raise a PR. 131 | 132 | # Acknowledgements 133 | 134 | This project is graciously supported by IDE tools offered by [JetBrains](https://www.jetbrains.com/) for development. 135 | 136 | [![alt tag](https://github.com/rcdexta/react-event-timeline/blob/master/jetbrains-badge.png)](https://www.jetbrains.com/) 137 | 138 | 139 | ## License 140 | 141 | MIT 142 | -------------------------------------------------------------------------------- /stories/App.story.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react' 2 | import React, { Component } from 'react' 3 | 4 | import { Timeline, TimelineBlip, TimelineEvent } from '../components' 5 | import Image from './sample.jpg' 6 | 7 | const globalStyles = { 8 | backgroundColor: 'rgb(255, 234, 234)', 9 | height: '100vh', 10 | fontFamily: 'Roboto' 11 | } 12 | 13 | const container = story =>
{story()}
14 | 15 | storiesOf('Timeline', module) 16 | .addDecorator(container) 17 | .add( 18 | 'Default View', 19 | () => ( 20 | 21 | textsms} 25 | iconColor='#6fba1c' 26 | > 27 | I received the payment for $543. Should be shipping the item within a 28 | couple of hours. Thanks for the order! 29 | 30 | email} 34 | iconColor='#03a9f4' 35 | > 36 |

Subject: Any updates?

37 |

38 | Like we talked, you said that you would share the shipment details? 39 | This is an urgent order and so I am losing patience. Can you 40 | expedite the process and pls do share the details asap. Consider 41 | this a gentle reminder if you are on track already! 42 |

43 |

- Maya

44 |
45 |
46 | ), 47 | { info: 'Timeline view with sensible defaults' } 48 | ) 49 | .add( 50 | 'Orientation', 51 | () => ( 52 | 53 | textsms} 57 | iconColor='#6fba1c' 58 | > 59 | I received the payment for $543. Should be shipping the item within a 60 | couple of hours. Thanks for the order! 61 | 62 | email} 66 | iconColor='#03a9f4' 67 | > 68 |

Subject: Any updates?

69 |

70 | Like we talked, you said that you would share the shipment details? 71 | This is an urgent order and so I am losing patience. Can you 72 | expedite the process and pls do share the details asap. Consider 73 | this a gentle reminder if you are on track already! 74 |

75 |

- Maya

76 |
77 |
78 | ), 79 | { info: 'Timeline view with the other orientation' } 80 | ) 81 | .add( 82 | 'Condensed Timeline', 83 | () => ( 84 | 85 | assignment_late} 89 | iconColor='#03a9f4' 90 | style={{ 91 | color: '#9c27b0' 92 | }} 93 | /> 94 | grade} 97 | iconColor='#6fba1c' 98 | /> 99 | 100 | ), 101 | { info: 'Use TimelineBlip to display micro events' } 102 | ) 103 | .add( 104 | 'Action buttons', 105 | () => ( 106 | 107 | textsms} 111 | buttons={ 112 | 113 | reply 114 | 115 | } 116 | iconColor='#6fba1c' 117 | > 118 | You should be receiving the shipment by tomorrow evening. Please reply 119 | back if you have more questions 120 | 121 | 122 | ), 123 | { info: 'Checkout the reply button to the top right corner of the event' } 124 | ) 125 | .add( 126 | 'Content with images', 127 | () => ( 128 | 129 | textsms} 133 | iconColor='#6fba1c' 134 | > 135 |

Please check if this image is good for printing

136 | 137 |
138 |
139 | ), 140 | { info: 'The event can contain any content include media' } 141 | ) 142 | .add( 143 | 'Card Appearance', 144 | () => ( 145 | 146 | event} 150 | iconColor='#757575' 151 | buttons={ 152 | 156 | play_circle_filled 157 | 158 | } 159 | container='card' 160 | style={{ 161 | boxShadow: '0 0 6px 1px #BD3B36', 162 | border: '1px solid #777', 163 | borderRadius: 3, 164 | fontWeight: 400, 165 | color: '#828282c' 166 | }} 167 | cardHeaderStyle={{ backgroundColor: '#8bc34a', color: '#503331' }} 168 | > 169 | Card as timeline event with custom container and header styling 170 | 171 | event} 175 | iconColor='#757575' 176 | buttons={ 177 | 181 | play_circle_filled 182 | 183 | } 184 | container='card' 185 | > 186 | A simple card with sensible defaults for styling 187 | 188 | 189 | ), 190 | { info: 'Timeline event container can be modelled as a card' } 191 | ) 192 | .add( 193 | 'Content Event handlers', 194 | () => ( 195 | 196 | textsms} 200 | iconColor='#6fba1c' 201 | onClick={() => alert('You clicked here!')} 202 | > 203 | Clicking this should raise an alert! 204 | 205 | 206 | ), 207 | { info: 'Timeline events can listen to user actions' } 208 | ) 209 | .add( 210 | 'Icon Event handlers', 211 | () => { 212 | class IconEventDemo extends React.Component { 213 | constructor(props) { 214 | super(props) 215 | this.state = { 216 | active: true, 217 | iconColor: 'red' 218 | } 219 | this.toggleEvent = this.toggleEvent.bind(this) 220 | } 221 | 222 | get icon() { 223 | return this.state.active ? 'stop' : 'play_arrow' 224 | } 225 | 226 | toggleEvent() { 227 | alert('Will toggle event status between play and stop') 228 | this.setState({ 229 | active: !this.state.active, 230 | iconColor: this.state.iconColor === 'red' ? 'green' : 'red' 231 | }) 232 | } 233 | 234 | render() { 235 | return ( 236 | 237 | {this.icon}} 241 | iconColor={this.state.iconColor} 242 | onIconClick={this.toggleEvent} 243 | > 244 | Press icon on the bubble to stop music 245 | 246 | 247 | ) 248 | } 249 | } 250 | return 251 | }, 252 | { info: 'Timeline events can listen to user actions' } 253 | ) 254 | .add( 255 | 'Event Styling', 256 | () => ( 257 | 258 | textsms} 262 | iconColor='#6fba1c' 263 | style={{ 264 | backgroundColor: '#fff', 265 | padding: 10, 266 | boxShadow: '0 0 3px 1px #BD3B36', 267 | border: '1px solid #eee' 268 | }} 269 | contentStyle={{ backgroundColor: '#00BCD4', color: '#fff' }} 270 | > 271 | This message should appear on a different background 272 | 273 | 274 | ), 275 | { info: 'TimelineEvent is completely customizable' } 276 | ) 277 | .add( 278 | 'Icon and Bubble Styling', 279 | () => ( 280 | 281 | textsms} 285 | iconColor='#6fba1c' 286 | bubbleStyle={{ backgroundColor: '#00ff81' }} 287 | > 288 | Notice the style variations to the bubble and icons 289 | 290 | phone} 294 | iconStyle={{ marginLeft: 1, marginTop: 0 }} 295 | iconColor='#5C6BC0' 296 | > 297 | John called! 298 | 299 | 300 | ), 301 | { info: 'Modify the appearance of bubbles and containing icons' } 302 | ) 303 | .add( 304 | 'Dynamic Prop Updates DontTest ', 305 | () => { 306 | class DynamicTimeline extends Component { 307 | constructor(props) { 308 | super(props) 309 | this.state = { 310 | createdAt: new Date() 311 | } 312 | this.onClick = this.onClick.bind(this) 313 | } 314 | 315 | onClick() { 316 | this.setState({ createdAt: new Date() }) 317 | } 318 | 319 | render() { 320 | return ( 321 |
322 | 323 | textsms} 327 | iconColor='#6fba1c' 328 | > 329 | This message should appear on a different background 330 | 331 | 332 | 335 |
336 | ) 337 | } 338 | } 339 | return 340 | }, 341 | { info: 'Props passed to the events are updated in realtime' } 342 | ) 343 | .add( 344 | 'Title and Subtitle styling', 345 | () => ( 346 | 347 | textsms} 352 | iconColor='#6fba1c' 353 | > 354 | I received the payment for $543. Should be shipping the item within a 355 | couple of hours. Thanks for the order! 356 | 357 | email} 363 | iconColor='#03a9f4' 364 | > 365 | Like we talked, you said that you would share the shipment details? 366 | This is an urgent order and so I am losing patience. Can you expedite 367 | the process and pls do share the details asap. Consider this a gentle 368 | reminder if you are on track already! 369 | 370 | 371 | ), 372 | { info: 'Add your own title and subtitle to events' } 373 | ) 374 | .add( 375 | 'TimelineEvent with collapsible content', 376 | () => ( 377 | 378 | textsms} 382 | iconColor='#6fba1c' 383 | collapsible 384 | showContent 385 | > 386 | I received the payment for $543. Should be shipping the item within a 387 | couple of hours. Thanks for the order! 388 | 389 | email} 393 | iconColor='#03a9f4' 394 | collapsible 395 | > 396 |

Subject: Any updates?

397 |

398 | Like we talked, you said that you would share the shipment details? 399 | This is an urgent order and so I am losing patience. Can you 400 | expedite the process and pls do share the details asap. Consider 401 | this a gentle reminder if you are on track already! 402 |

403 |

- Maya

404 |
405 |
406 | ), 407 | { info: 'Make the content collapsible' } 408 | ) 409 | --------------------------------------------------------------------------------