├── .babelrc ├── .circleci └── config.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── eslintrc.js ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── icon.ai ├── index.html ├── manifest.json ├── props.csv ├── sample_data.js └── screenshot.png ├── src ├── App.css ├── App.test.js ├── App.tsx ├── assets │ ├── background-texture.jpg │ ├── berlin.jpg │ ├── berlin_normalized.jpg │ ├── berlin_odd.jpg │ ├── cairo.jpg │ ├── cairo_normalized.jpg │ ├── cairo_odd.jpg │ ├── chicago.jpg │ ├── chicago_normalized.jpg │ ├── chicago_odd.jpg │ ├── icon.png │ ├── london.jpg │ ├── london_normalized.jpg │ ├── london_odd.jpg │ ├── madrid.jpg │ ├── madrid_normalized.jpg │ ├── madrid_odd.jpg │ ├── ny.jpg │ ├── ny_normalized.jpg │ ├── ny_odd.jpg │ ├── paris.jpg │ ├── paris_normalized.jpg │ ├── paris_odd.jpg │ ├── placeholder.png │ ├── rome.jpg │ ├── rome_normalized.jpg │ ├── rome_odd.jpg │ ├── seoul.jpg │ ├── seoul_normalized.jpg │ ├── seoul_odd.jpg │ ├── tokyo.jpg │ ├── tokyo_normalized.jpg │ └── tokyo_odd.jpg ├── data.js ├── index.css ├── index.tsx ├── lib │ ├── timeline.scss │ ├── timeline.test.js │ └── timeline.tsx ├── react-app-env.d.ts ├── serviceWorker.js └── setupTests.js ├── tsconfig.json ├── tsconfig.sample ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | 4 | run_tests: 5 | 6 | parallelism: 1 7 | shell: /bin/bash --login 8 | docker: 9 | - image: circleci/node:10.20 10 | steps: 11 | - checkout 12 | - run: yarn 13 | - run: yarn test 14 | 15 | workflows: 16 | version: 2 17 | workflow: 18 | jobs: 19 | - run_tests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | # typescript defs 23 | src/App.d.ts 24 | src/index.d.ts 25 | src/lib/timeline.d.ts 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | assets 2 | dist-example 3 | src 4 | node_modules 5 | test 6 | lib 7 | .idea 8 | .babelrc 9 | .gitignore 10 | example.js 11 | index.js 12 | server.js 13 | webpack.*% 14 | webpack.* 15 | public 16 | build 17 | tsconfig.sample 18 | tsconfig.json 19 | eslintrc.js 20 | yarn.lock 21 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "printWidth": 120 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Aaron Geisler 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/aaron9000/react-image-timeline/tree/master.svg?style=svg)](https://circleci.com/gh/aaron9000/react-image-timeline/tree/master) 2 | 3 | # React Image Timeline 4 | 5 | An image-centric timeline component for React.js. View chronological events in a pleasant way. 6 | 7 | [View Sample Timeline](http://aaron9000.github.io/react-image-timeline/) 8 | 9 | ### Features: 10 | 11 | - Responsive layout 12 | - Graceful handling of non-uniform content 13 | - Customizable (use your own CSS and components) 14 | - Memoized, pure, & typed (Typescript definitions included) 15 | - Only 32kb 16 | - ***Zero*** extra dependencies 17 | 18 | ![screenshot](https://github.com/aaron9000/react-image-timeline/blob/master/public/screenshot.png?raw=true) 19 | 20 | ## How to Use 21 | 22 | `npm install react-image-timeline --save` 23 | 24 | ```js 25 | import React from 'react'; 26 | import ReactDOM from 'react-dom'; 27 | import Timeline from 'react-image-timeline'; 28 | require('react-image-timeline/dist/timeline.css'); // .scss also available 29 | 30 | const events = [ 31 | { 32 | date: new Date(2013, 9, 27), 33 | text: "Sed leo elit, pellentesque sit amet congue quis, ornare nec lorem.", 34 | title: "Cairo, Egypt", 35 | buttonText: 'Click Me', 36 | imageUrl: "http://github.com/aaron9000/react-image-timeline/blob/master/src/assets/cairo.jpg?raw=true", 37 | onClick: console.log, 38 | } 39 | ]; 40 | 41 | ReactDOM.render(, document.getElementById('root')); 42 | ``` 43 | 44 | ## Customization 45 | 46 | #### Custom Styles 47 | To customize the timeline, add your own CSS to override the [default styles](https://github.com/aaron9000/react-image-timeline/blob/master/src/lib/timeline.scss/). 48 | 49 | #### Event Metadata 50 | To pass extra data into custom components, use `extras` on `TimelineEvent`. 51 | 52 | #### Custom Dot Pattern 53 | The dots are defined in CSS using a [base64-encoded image](https://www.base64-image.de/). Encode a new image and override the corresponding CSS class. 54 | 55 | #### Custom Components 56 | For more advanced customization, you can pass in custom components to replace the defaults. Custom components will be passed a `TimelineEvent` model in props. 57 | ```js 58 | 59 | const CustomHeader = (props) => { 60 | 61 | const {title, extras} = props.event; 62 | const {customField} = extras; 63 | 64 | return
65 |

{title}

66 |

{customField}

67 |
; 68 | }; 69 | 70 | ReactDOM.render(, document.getElementById('root')); 71 | ``` 72 | 73 | --- 74 | 75 | #### Run Example Project (you will need `create-react-app` to run) 76 | ``` 77 | *install create-react-app* 78 | *clone repository* 79 | yarn 80 | yarn --debug 81 | yarn start 82 | ``` 83 | 84 | #### Run Tests 85 | ``` 86 | *clone repository* 87 | yarn test 88 | ``` 89 | 90 | 91 | 92 | ## TypeScript & Models 93 | 94 | Typescript definitions are included in the library. 95 | 96 | --- 97 | 98 | #### Importing TypeScript Definitions 99 | 100 | ```js 101 | import { 102 | TimelineProps, 103 | TimelineEventProps, 104 | TimelineEvent, 105 | TimelineCustomComponents 106 | } from 'react-image-timeline'; 107 | ``` 108 | 109 | --- 110 | 111 | #### TimelineProps 112 | 113 | ```js 114 | export interface TimelineProps { 115 | customComponents?: TimelineCustomComponents | null; 116 | events: Array; 117 | reverseOrder?: boolean; 118 | denseLayout?: boolean; 119 | } 120 | ``` 121 | 122 | | Key | Type | Required? 123 | |--------------------------|--------------------------|--------------------------| 124 | | events | Array | Yes | 125 | | customComponents |TimelineCustomComponents | | 126 | | reverseOrder | boolean | | 127 | | denseLayout | boolean | | 128 | 129 | --- 130 | 131 | #### TimelineCustomComponents 132 | 133 | ```js 134 | export interface TimelineCustomComponents { 135 | topLabel?: React.PureComponent | React.ReactNode | null; 136 | bottomLabel?: React.PureComponent | React.ReactNode | null; 137 | header?: React.PureComponent | React.ReactNode | null; 138 | imageBody?: React.PureComponent | React.ReactNode | null; 139 | textBody?: React.PureComponent | React.ReactNode | null; 140 | footer?: React.PureComponent | React.ReactNode | null; 141 | } 142 | ``` 143 | 144 | | Key | Type | Required? 145 | |--------------------------|--------------------------|--------------------------| 146 | | topLabel | component | | 147 | | bottomLabel | component | | 148 | | header | component | | 149 | | imageBody | component | | 150 | | textBody | component | | 151 | | footer | component | | 152 | 153 | --- 154 | 155 | #### TimelineEventProps 156 | 157 | ```js 158 | export interface TimelineEventProps { 159 | event: TimelineEvent; 160 | } 161 | ``` 162 | 163 | | Key | Type | Required? 164 | |--------------------------|--------------------------|--------------------------| 165 | | event | TimelineEvent | Yes | 166 | 167 | --- 168 | 169 | #### TimelineEvent 170 | 171 | ```js 172 | export interface TimelineEvent { 173 | date: Date; 174 | title: string; 175 | imageUrl: string; 176 | text: string; 177 | onClick?: TimelineEventClickHandler | null; 178 | buttonText?: string | null; 179 | extras?: object | null; 180 | } 181 | ``` 182 | 183 | | Key | Type | Required? 184 | |--------------------------|--------------------------|--------------------------| 185 | | date | date | Yes | 186 | | title | string | Yes | 187 | | imageUrl | string | Yes | 188 | | text | string | Yes | 189 | | onClick | function | | 190 | | buttonText | string | | 191 | | extras | object | | 192 | 193 | 194 | -------------------------------------------------------------------------------- /eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser 3 | extends: [ 4 | "plugin:@typescript-eslint/recommended" // Uses the recommended rules from the @typescript-eslint/eslint-plugin 5 | ], 6 | parserOptions: { 7 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 8 | sourceType: "module" // Allows for the use of imports 9 | }, 10 | rules: { 11 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 12 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 13 | } 14 | }; -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/timeline.d.ts'); 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/timeline'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-image-timeline", 3 | "version": "3.2.13", 4 | "main": "index.js", 5 | "types": "dist/timeline.d.ts", 6 | "homepage": "http://aaron9000.github.io/react-image-timeline", 7 | "dependencies": {}, 8 | "devDependencies": { 9 | "@babel/cli": "^7.2.3", 10 | "@babel/core": "^7.2.2", 11 | "@babel/preset-env": "^7.3.1", 12 | "@babel/preset-react": "^7.0.0", 13 | "@types/react": "^16.9.11", 14 | "@types/react-dom": "^16.9.3", 15 | "chai": "^4.2.0", 16 | "enzyme": "^3.8.0", 17 | "enzyme-adapter-react-16": "^1.8.0", 18 | "gh-pages": "^2.0.1", 19 | "node-sass": "^4.14.1", 20 | "ramda": "^0.26.1", 21 | "react": "^16.7.0", 22 | "react-dom": "^16.7.0", 23 | "react-scripts": "3.4.0", 24 | "sinon": "^7.2.3", 25 | "source-map-loader": "^0.2.4", 26 | "svg-inline-loader": "^0.8.2", 27 | "ts-loader": "^6.2.1", 28 | "typescript": "^3.6.4", 29 | "webpack-cli": "^3.3.11" 30 | }, 31 | "peerDependencies": { 32 | "react": "^16.0.0" 33 | }, 34 | "scripts": { 35 | "start": "react-scripts start", 36 | "build": "react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject", 39 | "build-module": "node-sass src/lib/timeline.scss dist/timeline.css && cp src/lib/timeline.scss dist/timeline.scss && webpack && cp src/lib/timeline.d.ts dist/timeline.d.ts", 40 | "build-and-publish": "npm version patch && npm run build-module && npm publish", 41 | "deploy-gh-pages": "gh-pages -d build", 42 | "build-and-deploy": "npm run build && npm run deploy-gh-pages" 43 | }, 44 | "eslintConfig": { 45 | "extends": "react-app" 46 | }, 47 | "browserslist": [ 48 | ">0.2%", 49 | "not dead", 50 | "not ie <= 11", 51 | "not op_mini all" 52 | ], 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/aaron9000/react-image-timeline.git" 56 | }, 57 | "keywords": [ 58 | "react image timeline", 59 | "image timeline", 60 | "timeline", 61 | "react", 62 | "component", 63 | "image", 64 | "time", 65 | "line" 66 | ], 67 | "author": "Aaron Geisler", 68 | "license": "MIT", 69 | "bugs": { 70 | "url": "https://github.com/aaron9000/react-image-timeline/issues" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/public/favicon.ico -------------------------------------------------------------------------------- /public/icon.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/public/icon.ai -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "example", 3 | "name": "react-image-timeline example", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/props.csv: -------------------------------------------------------------------------------- 1 | Key,Type,Required 2 | events,array,required 3 | reverseOrder,boolean, 4 | customStartLabel,component, 5 | customEndLabel,component, 6 | customHeader,component, 7 | customImageBody,component, 8 | customTextBody,component, 9 | customFooter,component, 10 | date,date,required 11 | title,string,required 12 | imageUrl,string,required 13 | text,string,required 14 | onClick,function, 15 | buttonText,string, 16 | extras,object, -------------------------------------------------------------------------------- /public/sample_data.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { 3 | date: Date.parse("2013-05-15T07:00:00.000Z"), 4 | text: 5 | "Sed leo elit, pellentesque sit amet congue quis, ornare nec lorem. Phasellus tincidunt rhoncus magna,\neget elementum odio rutrum fermentum. Ut a justo lacus. Maecenas blandit molestie felis ac viverra. Pellentesque\nsagittis ligula neque, sit amet feugiat massa tempor sed. Duis id bibendum ex, pulvinar tincidunt ", 6 | title: "Berlin, Germany", 7 | imageUrl: 8 | "http://github.com/aaron9000/react-image-timeline/src/assets/berlin.jpg?raw=true" 9 | }, 10 | { 11 | date: Date.parse("2013-08-14T07:00:00.000Z"), 12 | text: 13 | "Sed leo elit, pellentesque sit amet congue quis, ornare nec lorem. Phasellus tincidunt rhoncus magna,\neget elementum odio rutrum fermentum. Ut a justo lacus. Maecenas blandit molestie felis ac viverra. Pellentesque\nsagittis ligula neque, sit amet feugiat massa tempor sed. Duis id bibendum ex, pulvinar tincidunt orci. Curabitur\nid sem urna. Maecenas sed elit malesuada, cursus ligula ut, vestibulum lorem. Suspendisse vitae ultrices libero.\nMauris maximus, ligula vitae tincidunt scelerisque, ipsum magna euismod massa, eu vulputate enim est tempus sem.\nMaecenas id nibh vitae ante volutpat facilisis nec eget velit. Proin et ligula feugiat, auctor tellus sit amet,\naccumsan neque. Quisque porttitor lectus quis elit fermentum, a facilisis est suscipit. Integer consectetur dapibus\nnisi, ut lacinia enim vulputate vitae. Curabitur id diam mauris. Duis dictum, dolor at porttitor aliquet, justo libero\nmattis magna, eu pellentesque augue mauris eget erat. Pellentesque lacinia velit nec ullamcorper mollis. Pellentesque\nlacus tortor, m", 14 | title: "Chicago, Illinois", 15 | imageUrl: 16 | "http://github.com/aaron9000/react-image-timeline/src/assets/chicago.jpg?raw=true" 17 | }, 18 | { 19 | date: Date.parse("2013-09-27T07:00:00.000Z"), 20 | text: 21 | "Sed leo elit, pellentesque sit amet congue quis, ornare nec lorem. Phasellus tincidunt rhoncus magna,\neget elementum odio rutrum fermentum. Ut a justo lacus. Maecenas blandit molestie felis ac viverra. Pellentesque\nsagittis ligula neque, sit amet feugiat massa tempor sed. Duis id bibendum ex, pulvinar tincidunt orci. Curabitur\nid sem urna. Maecenas sed elit malesuada, cursus ligula ut, vestibulum lorem. Suspendisse vitae ultric", 22 | title: "Cairo, Egypt", 23 | imageUrl: 24 | "http://github.com/aaron9000/react-image-timeline/src/assets/egypt.jpg?raw=true" 25 | }, 26 | { 27 | date: Date.parse("2013-12-10T08:00:00.000Z"), 28 | text: 29 | "Sed leo elit, pellentesque sit amet congue quis, ornare nec lorem. Phasellus tincidunt rhoncus magna,\neget elementum odio rutrum fermentum. Ut a justo lacus. Maecenas blandit molestie felis ac viverra. Pellentesque\nsagittis ligula neque, sit amet feugiat massa tempor sed. Duis id bibendum ex, pulvinar tincidunt orci. Curabitur\nid sem urna. Maecenas sed elit malesuada, cursus ligula ut, vestibulum lorem. Suspendisse vitae ultrices libero.\nMauris maximus, ligula vitae tincidunt scelerisque, ipsum magna euismod massa, eu vulputate enim est tempus sem.\nMaecenas id nibh vitae ante volutpat facilisis nec eget velit. Proin et ligula feugiat, auctor tellus sit amet,\naccumsan neque. Quisque porttitor lectus quis elit fermentum, a facilisis est suscipit. Integer consectetur dapibus\nnisi, ut lacinia enim vulputate vitae. Curabitur id diam mauris. Duis dictum, dolor at porttitor aliquet, justo libero\nmattis magna, eu pellentesque augue mau", 30 | title: "London, England", 31 | imageUrl: 32 | "http://github.com/aaron9000/react-image-timeline/src/assets/london.jpg?raw=true" 33 | }, 34 | { 35 | date: Date.parse("2014-01-12T08:00:00.000Z"), 36 | text: 37 | "Sed leo elit, pellentesque sit amet congue quis, ornare nec lorem. Phasellus tincidunt rhoncus magna,\neget elementum odio rutrum fermentum. Ut a justo lacus. Maecenas blandit molestie felis ac viverra. Pellentesque\nsagittis ligula neque, sit amet feugiat massa tempor sed. Duis id bibendum ex, pulvinar tincidunt orci. Curabitur\nid sem urna. Maecenas sed elit malesuada, cursus ligula ut, vestibulum lorem. Suspendisse vitae ultrices libero.\nMauris maximus, ligula vitae tincidunt scelerisque, ipsum magna euismod massa, eu vulputate enim est tempus sem.\nMaecenas id nibh vitae ante volutpat facilisis nec eget velit. Proin et ligula feugiat, auctor tellus sit amet,\naccumsan neque. Quisque porttitor lectus quis elit fermentum, a facilisis est suscipit. Integer consectetur dapibus\nnisi, ut lacinia enim vulputate vitae. Curabitur id diam mauris. Duis dictum, dolor at porttitor aliquet, justo libero\nmattis magna, eu pellentesque augue mauris eget erat. Pellentesque lacinia velit nec ullamcorper mollis. Pellentesque\nlacus tortor, maximus eget tincidunt non, luctus scelerisque odio. Suspendisse potent", 38 | title: "New York, New York", 39 | imageUrl: "http://github.com/aaron9000/react-image-timeline/src/assets/ny.jpg?raw=true" 40 | }, 41 | { 42 | date: Date.parse("2014-03-19T07:00:00.000Z"), 43 | text: 44 | "Sed leo elit, pellentesque sit amet congue quis, ornare nec lorem. Phasellus tincidunt rhoncus magna,\neget elementum odio rutrum fermentum. Ut a justo lacus. Maecenas blandit molestie felis ac viverra. Pellentesque\nsagittis ligula neque, sit amet feugiat massa tempor sed. Duis id bibendum ex, pulvinar tincidunt orci. Curabitur\nid sem urna. Maecenas sed elit malesuada, cursus ligula ut, vestibulum lorem. Suspendisse vitae ultrices libero.\nMauris maximus, ligula vitae tincidunt scelerisque, ipsum magna euismod massa, eu vulputate enim est tempus sem.\nMaecenas id nibh vitae ante volutpat facilisis nec eget velit. Proin et ligula feugiat, auctor tellus sit amet,\naccumsan neque. Quisque porttitor lectus quis elit fermentum, a facilisis est suscipit. Integer consectetur dapibus\nnisi, ut lacinia enim vulputate vitae. Curabitur id diam mauris. Duis dictum, dolor at porttitor aliquet, justo libero\nmattis magna, eu pellentesque augue mauris eget erat. Pellentesque lacinia velit nec ullamcorper mollis. Pellentesque\nlacus tortor, maximus eget tincidunt non, luctus scelerisque odio. Suspendisse potenti. Etiam vel augue blandit, auctor\nsem sit amet, imperdiet dolor.", 45 | title: "Paris, France", 46 | imageUrl: 47 | "http://github.com/aaron9000/react-image-timeline/src/assets/paris.jpg?raw=true" 48 | }, 49 | { 50 | date: Date.parse("2014-04-05T07:00:00.000Z"), 51 | text: 52 | "Sed leo elit, pellentesque sit amet congue quis, ornare nec lorem. Phasellus tincidunt rhoncus magna,\neget elementum odio rutrum fermentum. Ut a justo lacus. Maecenas blandit molestie felis ac viverra. Pellentesque\nsagittis ligula neque, sit amet feugiat massa tempor sed. Duis id bibendum ex, pulvinar tincidunt orci. Curabitur\nid sem urna. Maecenas sed elit malesuada, cursus ligula ut, vestibulum lorem. Suspendisse vitae ultrices libero.\nMauris maximus, ligula vitae tincidunt scelerisque, ipsum magna euismod massa, eu vulputate enim est tempus sem.\nMaecenas id nibh vitae ante vo", 53 | title: "Rome, Italy", 54 | imageUrl: "http://github.com/aaron9000/react-image-timeline/src/assets/rome.jpg?raw=true" 55 | }, 56 | { 57 | date: Date.parse("2014-06-12T07:00:00.000Z"), 58 | text: 59 | "Sed leo elit, pellentesque sit amet congue quis, ornare nec lorem. Phasellus tincidunt rhoncus magna,\neget elementum odio rutrum fermentum. Ut a justo lacus. Maecenas blandit molestie felis ac viverra. Pellentesque\nsagittis ligula neque, sit amet feugiat massa tempor sed. Duis id bibendum ex, pulvinar tincidunt orci. Curabitur\nid sem urna. Maecenas sed elit malesuada, cursus ligula ut, vestibulum lorem. Suspendisse vitae ultrices libero.\nMauris maximus, ligula vitae tincidunt scelerisque, ipsum magna euismod massa, eu vulputate enim est tempus sem.\nMaecenas id nibh vitae ante volutpat facilisis nec eget velit. Proin et ligula feugiat, auctor tellus sit amet,\naccumsan neque. Quisque porttitor lectus quis elit fermentum, a facilisis est suscipit. Integer consectetur dapibus\nnisi, ut lacinia enim vulputate vitae. Curabitur id diam mauris. Duis dictum, dolor at porttitor aliquet, justo libero\nmattis magna, eu pellentesque augue mauris eget erat. Pellentesque lacinia velit nec ullamcorper mollis", 60 | title: "Seoul, South Korea", 61 | imageUrl: 62 | "http://github.com/aaron9000/react-image-timeline/src/assets/seoul.jpg?raw=true" 63 | }, 64 | { 65 | date: Date.parse("2014-08-22T07:00:00.000Z"), 66 | text: 67 | "Sed leo elit, pellentesque sit amet congue quis, ornare nec lorem. Phasellus tincidunt rhoncus magna,\neget elementum odio rutrum fermentum. Ut a justo lacus. Maecenas blandit molestie felis ac viverra. Pellentesque\nsagittis ligula neque, sit amet feugiat massa tempor sed. Duis id bibendum ex, pulvinar tincidunt orci. Curabitur\nid sem urna. Maecenas sed elit malesuada, cursus ligula ut, vestibulum lorem. Suspendisse vitae ultrices libero.\nMauris maximus, ligula vitae tincidunt scelerisque, ipsum magna euismod massa, eu vulputate enim est tempus sem.\nMaecenas id nibh vitae ante volutpat facilisis nec eget velit. Proin et ligula feugiat, auctor tellus sit amet,\naccumsan neque. Quisque porttitor lectus quis elit fermentum, a facilisis est suscipit. Integer consectetur dapibus\nnisi, ut lacinia enim vulputate vitae. Curabitur id diam mauris. Duis dictum, dolor at porttitor aliquet, justo libero\nmattis magna, eu pellentesque augue mauris eget erat. Pellentesque lacinia velit nec ullamcorper mollis. Pellentesque\nlacus tortor, maxim", 68 | title: "Madrid, Spain", 69 | imageUrl: 70 | "http://github.com/aaron9000/react-image-timeline/src/assets/madrid.jpg?raw=true" 71 | }, 72 | { 73 | date: Date.parse("2014-10-05T07:00:00.000Z"), 74 | text: 75 | "Sed leo elit, pellentesque sit amet congue quis, ornare nec lorem. Phasellus tincidunt rhoncus magna,\neget elementum odio rutrum fermentum. Ut a justo lacus. Maecenas blandit molestie felis ac viverra. Pellentesque\nsagittis ligula neque, sit amet feugiat massa tempor sed. Duis id bibendum ex, pulvinar tincidunt orci. Curabitur\nid sem urna. Maecenas sed elit malesuada, cursus ligula ut, vestibulum lorem. Suspendisse vitae ultrices libero.\nMauris maximus, ligula vitae tincidunt scelerisque, ipsum magna euismod massa, eu vulputate enim est tempus sem.\nMaecenas id nibh vitae ante volutpat facilisis nec eget velit. Proin et ligula feugiat, auctor tellus sit amet,\naccumsan neque. Quisque porttitor lectus quis elit fermentum, a facilisis est suscipit. Integer consectetur dapibus\nnisi, ut lacinia enim vulputate vitae. Curabitur id diam mauris. Duis dictum, dolor at porttitor aliquet, justo libero\nmattis magna, eu pellentesque augue mauris eget erat. Pellente", 76 | title: "Tokyo, Japan", 77 | imageUrl: 78 | "http://github.com/aaron9000/react-image-timeline/src/assets/tokyo.jpg?raw=true" 79 | } 80 | ]; 81 | -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/public/screenshot.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, 'Arial', 'sans-serif'; 3 | font-weight: 300; 4 | color: #313740; 5 | background: url('./assets/background-texture.jpg'); 6 | } 7 | 8 | h3 { 9 | font-size: 2em; 10 | text-align: center; 11 | } 12 | 13 | h5 { 14 | font-size: 1.5em; 15 | text-align: center; 16 | } 17 | 18 | .config-container { 19 | display: flex; 20 | justify-content: center; 21 | } 22 | 23 | .toggle-container { 24 | padding: 0.5em; 25 | margin-top: 0.5em; 26 | margin-bottom: 0.5em; 27 | width: 320px; 28 | border: solid 1px lightgray; 29 | } 30 | 31 | .custom-top-label { 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | text-align: center; 36 | color: white; 37 | border-radius: 50%; 38 | background: #2d384b; 39 | font-size: 2em; 40 | box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.4); 41 | width: 120px; 42 | height: 120px; 43 | } 44 | 45 | .custom-bottom-label { 46 | display: flex; 47 | justify-content: center; 48 | align-items: center; 49 | text-align: center; 50 | color: white; 51 | border-radius: 50%; 52 | background: #2d384b; 53 | font-size: 2em; 54 | box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.4); 55 | width: 120px; 56 | height: 120px; 57 | } 58 | 59 | .custom-header { 60 | color: white; 61 | } 62 | 63 | .custom-header h3 { 64 | color: white; 65 | margin-bottom: 10px; 66 | margin-top: 10px; 67 | } 68 | 69 | .image-body-label { 70 | width: 100%; 71 | padding: 0.5em; 72 | color: white; 73 | position: absolute; 74 | text-align: center; 75 | background: black; 76 | } 77 | 78 | .custom-footer { 79 | color: white; 80 | font-weight: bold; 81 | width: 100%; 82 | text-align: center; 83 | background-color: #313740; 84 | } 85 | 86 | .custom-image-body { 87 | background-color: lightgray; 88 | } 89 | 90 | .custom-text-body { 91 | font-size: 1em; 92 | background-color: white; 93 | height: 200px; 94 | } 95 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | describe('', () => { 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render(, div); 9 | ReactDOM.unmountComponentAtNode(div); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import './App.css'; 3 | import './lib/timeline.scss'; 4 | import Timeline, { TimelineEvent, TimelineCustomComponents, TimelineEventProps } from './lib/timeline'; 5 | // @ts-ignore 6 | import { getSampleData } from './data'; 7 | 8 | const CustomTopLabel = (props: TimelineEventProps) => { 9 | return ( 10 |
11 |

Top Label

12 |
13 | ); 14 | }; 15 | 16 | const CustomBottomLabel = (props: TimelineEventProps) => { 17 | return ( 18 |
19 |

Bottom Label

20 |
21 | ); 22 | }; 23 | 24 | const CustomHeader = (props: TimelineEventProps) => { 25 | return ( 26 |
27 |

Header

28 |
29 | ); 30 | }; 31 | 32 | const CustomFooter = (props: TimelineEventProps) => { 33 | return ( 34 |
35 |

Footer

36 |
37 | ); 38 | }; 39 | 40 | const CustomTextBody = (props: TimelineEventProps) => { 41 | return ( 42 |
43 |

Text Body

44 |
45 | ); 46 | }; 47 | 48 | const CustomImageBody = (props: TimelineEventProps) => { 49 | const { imageUrl } = props.event; 50 | return ( 51 |
52 |

Image Body

53 | 54 |
55 | ); 56 | }; 57 | 58 | const TimelineExample = React.memo( 59 | ( 60 | props: {}, 61 | state: { 62 | events: Array; 63 | useCustomComponents: boolean; 64 | reverseOrder: boolean; 65 | denseLayout: boolean; 66 | imageType: string; 67 | } 68 | ) => { 69 | const [imageType, setImageType] = useState('normal'); 70 | const [events, setEvents] = useState(getSampleData(imageType)); 71 | const [useCustomComponents, setUseCustomComponents] = useState(false); 72 | const [reverseOrder, setReverseOrder] = useState(false); 73 | const [denseLayout, setDenseLayout] = useState(false); 74 | 75 | const onToggleReverseOrder = useCallback(() => setReverseOrder(!reverseOrder), [reverseOrder]); 76 | const onToggleDenseLayout = useCallback(() => setDenseLayout(!denseLayout), [denseLayout]); 77 | const onToggleUseCustomComponents = useCallback(() => setUseCustomComponents(!useCustomComponents), [ 78 | useCustomComponents, 79 | ]); 80 | const onToggleImageType = useCallback(() => { 81 | const newImageType = imageType === 'normal' ? 'odd' : 'normal'; 82 | setImageType(newImageType); 83 | setEvents(getSampleData(newImageType)); 84 | }, [imageType]); 85 | 86 | const customComponents = useCustomComponents 87 | ? ({ 88 | topLabel: CustomTopLabel, 89 | bottomLabel: CustomBottomLabel, 90 | header: CustomHeader, 91 | imageBody: CustomImageBody, 92 | textBody: CustomTextBody, 93 | footer: CustomFooter, 94 | } as TimelineCustomComponents) 95 | : null; 96 | return ( 97 |
98 |
99 |

React Image Timeline

100 |

resize window to see mobile layout

101 |
102 |
103 |
104 |
105 | 106 | Use Custom Components 107 |
108 |
109 | 110 | Reverse Order 111 |
112 |
113 | 114 | Non-unform Images 115 |
116 |
117 | 118 | Dense Layout 119 |
120 |
121 |
122 |
123 | 124 |
125 | ); 126 | } 127 | ); 128 | 129 | const App = TimelineExample; 130 | 131 | export default App; 132 | -------------------------------------------------------------------------------- /src/assets/background-texture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/background-texture.jpg -------------------------------------------------------------------------------- /src/assets/berlin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/berlin.jpg -------------------------------------------------------------------------------- /src/assets/berlin_normalized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/berlin_normalized.jpg -------------------------------------------------------------------------------- /src/assets/berlin_odd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/berlin_odd.jpg -------------------------------------------------------------------------------- /src/assets/cairo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/cairo.jpg -------------------------------------------------------------------------------- /src/assets/cairo_normalized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/cairo_normalized.jpg -------------------------------------------------------------------------------- /src/assets/cairo_odd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/cairo_odd.jpg -------------------------------------------------------------------------------- /src/assets/chicago.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/chicago.jpg -------------------------------------------------------------------------------- /src/assets/chicago_normalized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/chicago_normalized.jpg -------------------------------------------------------------------------------- /src/assets/chicago_odd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/chicago_odd.jpg -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/icon.png -------------------------------------------------------------------------------- /src/assets/london.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/london.jpg -------------------------------------------------------------------------------- /src/assets/london_normalized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/london_normalized.jpg -------------------------------------------------------------------------------- /src/assets/london_odd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/london_odd.jpg -------------------------------------------------------------------------------- /src/assets/madrid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/madrid.jpg -------------------------------------------------------------------------------- /src/assets/madrid_normalized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/madrid_normalized.jpg -------------------------------------------------------------------------------- /src/assets/madrid_odd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/madrid_odd.jpg -------------------------------------------------------------------------------- /src/assets/ny.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/ny.jpg -------------------------------------------------------------------------------- /src/assets/ny_normalized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/ny_normalized.jpg -------------------------------------------------------------------------------- /src/assets/ny_odd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/ny_odd.jpg -------------------------------------------------------------------------------- /src/assets/paris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/paris.jpg -------------------------------------------------------------------------------- /src/assets/paris_normalized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/paris_normalized.jpg -------------------------------------------------------------------------------- /src/assets/paris_odd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/paris_odd.jpg -------------------------------------------------------------------------------- /src/assets/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/placeholder.png -------------------------------------------------------------------------------- /src/assets/rome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/rome.jpg -------------------------------------------------------------------------------- /src/assets/rome_normalized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/rome_normalized.jpg -------------------------------------------------------------------------------- /src/assets/rome_odd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/rome_odd.jpg -------------------------------------------------------------------------------- /src/assets/seoul.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/seoul.jpg -------------------------------------------------------------------------------- /src/assets/seoul_normalized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/seoul_normalized.jpg -------------------------------------------------------------------------------- /src/assets/seoul_odd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/seoul_odd.jpg -------------------------------------------------------------------------------- /src/assets/tokyo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/tokyo.jpg -------------------------------------------------------------------------------- /src/assets/tokyo_normalized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/tokyo_normalized.jpg -------------------------------------------------------------------------------- /src/assets/tokyo_odd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaron9000/react-image-timeline/a2c08a4ce1ef558ccfd3b2d45020ed30cbe3717d/src/assets/tokyo_odd.jpg -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | import * as R from 'ramda'; 2 | 3 | import berlin from './assets/berlin.jpg'; 4 | import chicago from './assets/chicago.jpg'; 5 | import cairo from './assets/cairo.jpg'; 6 | import london from './assets/london.jpg'; 7 | import ny from './assets/ny.jpg'; 8 | import paris from './assets/paris.jpg'; 9 | import rome from './assets/rome.jpg'; 10 | import seoul from './assets/seoul.jpg'; 11 | import tokyo from './assets/tokyo.jpg'; 12 | import madrid from './assets/madrid.jpg'; 13 | 14 | import berlin_normalized from './assets/berlin_normalized.jpg'; 15 | import chicago_normalized from './assets/chicago_normalized.jpg'; 16 | import cairo_normalized from './assets/cairo_normalized.jpg'; 17 | import london_normalized from './assets/london_normalized.jpg'; 18 | import ny_normalized from './assets/ny_normalized.jpg'; 19 | import paris_normalized from './assets/paris_normalized.jpg'; 20 | import rome_normalized from './assets/rome_normalized.jpg'; 21 | import seoul_normalized from './assets/seoul_normalized.jpg'; 22 | import tokyo_normalized from './assets/tokyo_normalized.jpg'; 23 | import madrid_normalized from './assets/madrid_normalized.jpg'; 24 | 25 | import berlin_odd from './assets/berlin_odd.jpg'; 26 | import chicago_odd from './assets/chicago_odd.jpg'; 27 | import cairo_odd from './assets/cairo_odd.jpg'; 28 | import london_odd from './assets/london_odd.jpg'; 29 | import ny_odd from './assets/ny_odd.jpg'; 30 | import paris_odd from './assets/paris_odd.jpg'; 31 | import rome_odd from './assets/rome_odd.jpg'; 32 | import seoul_odd from './assets/seoul_odd.jpg'; 33 | import tokyo_odd from './assets/tokyo_odd.jpg'; 34 | import madrid_odd from './assets/madrid_odd.jpg'; 35 | 36 | const IMAGES = [ 37 | berlin, 38 | chicago, 39 | cairo, 40 | london, 41 | ny, 42 | paris, 43 | rome, 44 | seoul, 45 | madrid, 46 | tokyo, 47 | ]; 48 | 49 | const IMAGES_ODD = [ 50 | berlin_odd, 51 | chicago_odd, 52 | cairo_odd, 53 | london_odd, 54 | ny_odd, 55 | paris_odd, 56 | rome_odd, 57 | seoul_odd, 58 | madrid_odd, 59 | tokyo_odd, 60 | ]; 61 | 62 | const IMAGES_NORMALIZED = [ 63 | berlin_normalized, 64 | chicago_normalized, 65 | cairo_normalized, 66 | london_normalized, 67 | ny_normalized, 68 | paris_normalized, 69 | rome_normalized, 70 | seoul_normalized, 71 | madrid_normalized, 72 | tokyo_normalized, 73 | ]; 74 | 75 | const CITY_NAMES = [ 76 | 'Berlin, Germany', 77 | 'Chicago, Illinois', 78 | 'Cairo, Egypt', 79 | 'London, England', 80 | 'New York, New York', 81 | 'Paris, France', 82 | 'Rome, Italy', 83 | 'Seoul, South Korea', 84 | 'Madrid, Spain', 85 | 'Tokyo, Japan', 86 | ]; 87 | 88 | 89 | const IPSUM = `Sed leo elit, pellentesque sit amet congue quis, ornare nec lorem. Phasellus tincidunt rhoncus magna, 90 | eget elementum odio rutrum fermentum. Ut a justo lacus. Maecenas blandit molestie felis ac viverra. Pellentesque 91 | sagittis ligula neque, sit amet feugiat massa tempor sed. Duis id bibendum ex, pulvinar tincidunt orci. Curabitur 92 | id sem urna. Maecenas sed elit malesuada, cursus ligula ut, vestibulum lorem. Suspendisse vitae ultrices libero. 93 | Mauris maximus, ligula vitae tincidunt scelerisque, ipsum magna euismod massa, eu vulputate enim est tempus sem. 94 | Maecenas id nibh vitae ante volutpat facilisis nec eget velit. Proin et ligula feugiat, auctor tellus sit amet, 95 | accumsan neque. Quisque porttitor lectus quis elit fermentum, a facilisis est suscipit. Integer consectetur dapibus 96 | nisi, ut lacinia enim vulputate vitae. Curabitur id diam mauris. Duis dictum, dolor at porttitor aliquet, justo libero 97 | mattis magna, eu pellentesque augue mauris eget erat. Pellentesque lacinia velit nec ullamcorper mollis. Pellentesque 98 | lacus tortor, maximus eget tincidunt non, luctus scelerisque odio. Suspendisse potenti. Etiam vel augue blandit, auctor 99 | sem sit amet, imperdiet dolor. Ut a quam laoreet, feugiat orci sed, feugiat nulla. Nulla gravida nisi eu ex egestas 100 | dapibus.`; 101 | 102 | function getCitiesWithImages(type) { 103 | const zipFn = (title, imageUrl) => { 104 | return { title, imageUrl }; 105 | }; 106 | let images = IMAGES; 107 | if (type === 'normalized') { 108 | images = IMAGES_NORMALIZED; 109 | } else if (type === 'odd') { 110 | images = IMAGES_ODD; 111 | } 112 | return R.zipWith(zipFn, CITY_NAMES, images); 113 | } 114 | 115 | function addDays(date, days) { 116 | var result = new Date(date); 117 | result.setDate(result.getDate() + days); 118 | return result; 119 | }; 120 | 121 | function randomText(){ 122 | const minTextLength = 50; 123 | return R.slice(0, Math.random() * (R.length(IPSUM) - minTextLength) + minTextLength, IPSUM); 124 | } 125 | 126 | export function getSampleData(imageType) { 127 | const cities = getCitiesWithImages(imageType); 128 | const mapWithIndex = R.addIndex(R.map); 129 | return mapWithIndex((location, index) => { 130 | return R.merge( 131 | { 132 | date: addDays(new Date('2013-12-08'), index * 8), 133 | text: randomText(), 134 | buttonText: 'Read More', 135 | onClick: () => { 136 | const city = R.head(R.split(', ', location.title)); 137 | window.open(`https://wikipedia.org/wiki/${city}`); 138 | }, 139 | extras: { 140 | foo: '#Travel', 141 | }, 142 | }, 143 | location 144 | ); 145 | }, cities); 146 | } 147 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 5 | 'Droid Sans', 'Helvetica Neue', sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 12 | } 13 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | // @ts-ignore 6 | import * as serviceWorker from './serviceWorker'; 7 | 8 | ReactDOM.render(, document.getElementById('root')); 9 | 10 | // If you want your app to work offline and load faster, you can change 11 | // unregister() to register() below. Note this comes with some pitfalls. 12 | // Learn more about service workers: http://bit.ly/CRA-PWA 13 | serviceWorker.unregister(); 14 | -------------------------------------------------------------------------------- /src/lib/timeline.scss: -------------------------------------------------------------------------------- 1 | $max-image-height: 600px; 2 | $max-copy-height: 200px; 3 | $min-event-height: $max-image-height + $max-copy-height; 4 | $dual-min-width: 600px; 5 | $max-width: 1600px; 6 | $arrow-width: 5%; 7 | $event-width: 45%; 8 | $label-radius: 3em; 9 | $padding: 0.5em; 10 | $radius: 0.35em; 11 | $dot-radius: 5%; 12 | $arrow-offset: 20px; 13 | $fade-height: 80px; 14 | $text-color: #313740; 15 | $label-color: lighten(#2d384b, 15%); 16 | $event-color: #f0f0f0; 17 | $button-color: #5592cb; 18 | $header-color: lighten(#2d384b, 30%); 19 | $transparent-white: rgba(255, 255, 255, 0); 20 | 21 | *, 22 | *:after, 23 | *:before { 24 | -webkit-box-sizing: border-box; 25 | -moz-box-sizing: border-box; 26 | box-sizing: border-box; 27 | } 28 | 29 | h1, 30 | h2, 31 | h3, 32 | h4, 33 | h5, 34 | p { 35 | margin: 0; 36 | padding: 0; 37 | } 38 | 39 | h2 { 40 | font-size: 1.75em; 41 | } 42 | 43 | .rt-timeline-container { 44 | padding: 1em; 45 | justify-content: center; 46 | align-items: center; 47 | display: flex; 48 | } 49 | 50 | .rt-timeline { 51 | max-width: $max-width; 52 | padding: 0em; 53 | list-style-type: none; 54 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAA6ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNi0wNC0wM1QyMzowNDo4MjwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjQuMjwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8dGlmZjpDb21wcmVzc2lvbj41PC90aWZmOkNvbXByZXNzaW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj45PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj45PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cux0bOIAAABoSURBVBgZY2CgFmCEGVRYWCjk5ORUwcbG5vrr16/d+/bt6+jv738HkmeCKYIqKAXyDYAKS0F8mBxcEcgEmCCIRubDFYGsQFaEzGeGSUhISJyVk5NjYmZm5gYqWAxy04kTJ77D5KlDAwD54yLs/6kqyAAAAABJRU5ErkJggg=='); 55 | background-repeat: repeat-y; 56 | background-position: 50% 0; 57 | } 58 | 59 | .rt-label-container { 60 | clear: both; 61 | margin: 1em auto 1em auto; 62 | display: flex; 63 | justify-content: center; 64 | 65 | &:first-of-type { 66 | margin-top: 0 !important; 67 | } 68 | 69 | &:last-of-type { 70 | margin-bottom: 0; 71 | } 72 | } 73 | 74 | .rt-label { 75 | display: flex; 76 | justify-content: center; 77 | align-items: center; 78 | color: white; 79 | border-radius: 50%; 80 | background: $label-color; 81 | font-size: 2em; 82 | box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.4); 83 | width: $label-radius; 84 | height: $label-radius; 85 | } 86 | 87 | .rt-clear { 88 | clear: both; 89 | } 90 | 91 | .rt-backing { 92 | border-radius: $radius; 93 | background: $event-color; 94 | color: $text-color; 95 | box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.4); 96 | } 97 | 98 | .rt-event { 99 | position: relative; 100 | float: right; 101 | clear: right; 102 | width: $event-width; 103 | margin-top: 2em; 104 | margin-left: 0; 105 | margin-right: 0; 106 | list-style-type: none; 107 | display: block; 108 | min-height: $min-event-height; 109 | @media all and (max-width: $dual-min-width) { 110 | min-height: auto; 111 | width: 85%; 112 | svg { 113 | display: none; 114 | } 115 | 116 | .rt-dot { 117 | display: none; 118 | } 119 | } 120 | 121 | .rt-dot { 122 | position: absolute; 123 | margin-top: $arrow-offset; 124 | left: -(11% + ($dot-radius * 0.5)); 125 | right: auto; 126 | fill: currentcolor; 127 | color: $header-color; 128 | width: $dot-radius; 129 | z-index: 100; 130 | } 131 | 132 | .rt-svg-container { 133 | position: relative; 134 | 135 | svg { 136 | transform: scale(-1, 1); 137 | } 138 | 139 | .rt-arrow { 140 | z-index: 100; 141 | position: absolute; 142 | margin-top: $arrow-offset; 143 | left: -($arrow-width); 144 | right: auto; 145 | fill: currentcolor; 146 | width: $arrow-width + 0.25%; 147 | color: $header-color; 148 | } 149 | } 150 | 151 | &:nth-of-type(2n) { 152 | float: left; 153 | clear: left; 154 | text-align: right; 155 | 156 | svg { 157 | transform: scale(1, 1); 158 | } 159 | 160 | .rt-arrow { 161 | left: auto; 162 | right: -$arrow-width; 163 | } 164 | 165 | .rt-dot { 166 | left: auto; 167 | right: -(11% + ($dot-radius * 0.5)); 168 | } 169 | } 170 | } 171 | 172 | li:nth-child(3) { 173 | margin-top: ($max-copy-height + $max-image-height) * 0.5; 174 | 175 | @media all and (max-width: $dual-min-width) { 176 | margin-top: 1em; 177 | } 178 | } 179 | 180 | .rt-title { 181 | margin: 4px; 182 | font-weight: bold; 183 | } 184 | 185 | .rt-date { 186 | margin: 4px; 187 | font-weight: normal; 188 | } 189 | 190 | .rt-header-container { 191 | color: white; 192 | padding: $padding * 0.5; 193 | background-color: $header-color; 194 | border-radius: $radius $radius 0 0; 195 | overflow: hidden; 196 | } 197 | 198 | .rt-image-container { 199 | padding: 0; 200 | overflow: hidden; 201 | } 202 | 203 | .rt-image { 204 | vertical-align: top; 205 | display: flex; 206 | align-items: center; 207 | justify-content: center; 208 | margin: 0; 209 | width: 100%; 210 | height: auto; 211 | max-width: 100%; 212 | max-height: $max-image-height; 213 | object-fit: contain; 214 | } 215 | 216 | .rt-footer-container { 217 | padding: $padding; 218 | justify-content: center; 219 | align-items: center; 220 | display: flex; 221 | overflow: hidden; 222 | } 223 | 224 | .rt-btn { 225 | text-align: center; 226 | background-color: $text-color; 227 | font-size: 1.5em; 228 | font-weight: bold; 229 | cursor: pointer; 230 | white-space: nowrap; 231 | overflow: hidden; 232 | text-overflow: ellipsis; 233 | border-radius: $radius; 234 | padding: $padding; 235 | width: 100%; 236 | color: white; 237 | text-decoration: none; 238 | } 239 | 240 | .rt-text-container { 241 | padding: $padding; 242 | max-height: $max-copy-height; 243 | font-weight: lighter; 244 | overflow: hidden; 245 | text-overflow: ellipsis; 246 | content: ''; 247 | position: relative; 248 | text-align: left; 249 | } 250 | 251 | .rt-text-container:before { 252 | content: ''; 253 | font-weight: lighter; 254 | width: 100%; 255 | height: $fade-height; 256 | position: absolute; 257 | left: 0; 258 | top: $max-copy-height - $fade-height; 259 | background: transparent; 260 | background: -webkit-linear-gradient($transparent-white, $event-color); /* For Safari 5.1 to 6.0 */ 261 | background: -o-linear-gradient($transparent-white, $event-color); /* For Opera 11.1 to 12.0 */ 262 | background: -moz-linear-gradient($transparent-white, $event-color); /* For Firefox 3.6 to 15 */ 263 | background: linear-gradient($transparent-white, $event-color); /* Standard syntax */ 264 | } 265 | -------------------------------------------------------------------------------- /src/lib/timeline.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as R from 'ramda'; 3 | import sinon from 'sinon'; 4 | import { expect } from 'chai'; 5 | import { shallow, mount, render } from 'enzyme'; 6 | import Timeline from './timeline'; 7 | 8 | const SHUFFLED_EVENTS = [ 9 | { 10 | title: 'LAST', 11 | imageUrl: 'LAST', 12 | text: 'LAST', 13 | date: new Date(2010, 1, 11), 14 | extras: { foo: 'bar' }, 15 | }, 16 | { 17 | title: 'FIRST', 18 | imageUrl: 'FIRST', 19 | text: 'FIRST', 20 | date: new Date(2000, 1, 11), 21 | extras: { foo: 'bar' }, 22 | }, 23 | { 24 | title: 'MIDDLE', 25 | imageUrl: 'MIDDLE', 26 | text: 'MIDDLE', 27 | date: new Date(2005, 1, 11), 28 | extras: { foo: 'bar' }, 29 | }, 30 | ]; 31 | 32 | const SINGLE_INVALID_EVENT = [ 33 | { 34 | title: 'TITLE', 35 | imageUrl: 'IMAGE_URL', 36 | text: 'TEXT', 37 | date: new Date('asdfadf'), 38 | extras: null, 39 | }, 40 | ]; 41 | 42 | const MIXED_DATES = SINGLE_INVALID_EVENT.concat(SHUFFLED_EVENTS); 43 | 44 | const EMPTY_DIV =
; 45 | 46 | const CustomHeader = props => { 47 | return
*CustomHeader*
; 48 | }; 49 | 50 | const CustomImageBody = props => { 51 | return
*CustomImageBody*
; 52 | }; 53 | 54 | const CustomTextBody = props => { 55 | return
*CustomTextBody*
; 56 | }; 57 | 58 | const CustomFooter = props => { 59 | return
*CustomFooter*
; 60 | }; 61 | 62 | const CustomTopLabel = props => { 63 | return
*CustomTopLabel*
; 64 | }; 65 | 66 | const CustomBottomLabel = props => { 67 | return
*CustomBottomLabel*
; 68 | }; 69 | 70 | describe('', () => { 71 | 72 | describe('State', () => { 73 | it("receives 'extras' in 'events' prop", () => { 74 | const wrapper = mount(); 75 | const { extras } = R.head(wrapper.props().events); 76 | expect(extras.foo).to.equal('bar'); 77 | }); 78 | 79 | it('special cases 0 events', () => { 80 | expect(shallow().contains(EMPTY_DIV)).to.equal(true); 81 | }); 82 | 83 | it('filters events with invalid dates', () => { 84 | expect(shallow().contains(EMPTY_DIV)).to.equal(true); 85 | }); 86 | 87 | it('only filters invalid dates', () => { 88 | expect(shallow().contains(EMPTY_DIV)).to.equal(false); 89 | }); 90 | }); 91 | 92 | describe('Labels', () => { 93 | it('renders labels on the ends of the timeline', () => { 94 | const wrapper = shallow(); 95 | expect( 96 | wrapper 97 | .find('li') 98 | .first() 99 | .html() 100 | ).to.equal('
  • 2000
  • '); 101 | expect( 102 | wrapper 103 | .find('li') 104 | .last() 105 | .html() 106 | ).to.equal('
  • 2010
  • '); 107 | }); 108 | 109 | it('renders reversed labels', () => { 110 | const wrapper = shallow(); 111 | expect( 112 | wrapper 113 | .find('li') 114 | .first() 115 | .html() 116 | ).to.equal('
  • 2010
  • '); 117 | expect( 118 | wrapper 119 | .find('li') 120 | .last() 121 | .html() 122 | ).to.equal('
  • 2000
  • '); 123 | }); 124 | }); 125 | 126 | describe('Layout', () => { 127 | it('renders dense layout with no minum height', () => { 128 | const wrapper = shallow(); 129 | expect( 130 | wrapper 131 | .find('.rt-event') 132 | .get(0) 133 | .props 134 | .style 135 | ).to.deep.equal({ minHeight: 'auto' }); 136 | }); 137 | 138 | it('renders normal layout without a style override', () => { 139 | const wrapper = shallow(); 140 | expect( 141 | wrapper 142 | .find('.rt-event') 143 | .get(0) 144 | .props 145 | .style 146 | ).to.deep.equal({}); 147 | }); 148 | }); 149 | 150 | 151 | describe('Events', () => { 152 | it('renders events correctly', () => { 153 | const shallowWrapper = shallow(); 154 | const deepWrapper = render(); 155 | const assertClassCountDeep = (classes, count) => { 156 | R.map(c => { 157 | return expect(deepWrapper.find(c)).to.have.length(count); 158 | }, classes); 159 | }; 160 | const assertClassCountShallow = (classes, count) => { 161 | R.map(c => { 162 | return expect(shallowWrapper.find(c)).to.have.length(count); 163 | }, classes); 164 | }; 165 | assertClassCountShallow(['.rt-timeline', '.rt-timeline-container'], 1); 166 | assertClassCountDeep(['.rt-label-container'], 2); 167 | assertClassCountDeep( 168 | [ 169 | '.rt-event', 170 | '.rt-btn', 171 | '.rt-image-container', 172 | '.rt-text-container', 173 | '.rt-header-container', 174 | '.rt-footer-container', 175 | ], 176 | R.length(SHUFFLED_EVENTS) 177 | ); 178 | }); 179 | 180 | it('events call onClick when clicked', () => { 181 | const onClick = sinon.spy(); 182 | const spiedEvents = R.map(event => { 183 | return R.merge(event, { onClick }); 184 | }, SHUFFLED_EVENTS); 185 | const wrapper = mount(); 186 | wrapper 187 | .find('button') 188 | .first() 189 | .simulate('click'); 190 | expect(onClick.calledOnce).to.equal(true); 191 | }); 192 | 193 | it('renders events in order with unordered data', () => { 194 | const wrapper = shallow(); 195 | expect( 196 | wrapper 197 | .find('.rt-event') 198 | .first() 199 | .html() 200 | .indexOf('FIRST') 201 | ).to.be.above(-1); 202 | expect( 203 | wrapper 204 | .find('.rt-event') 205 | .last() 206 | .html() 207 | .indexOf('LAST') 208 | ).to.be.above(-1); 209 | }); 210 | 211 | it('renders events in reverse-order with unordered data', () => { 212 | const wrapper = shallow(); 213 | expect( 214 | wrapper 215 | .find('.rt-event') 216 | .first() 217 | .html() 218 | .indexOf('LAST') 219 | ).to.be.above(-1); 220 | expect( 221 | wrapper 222 | .find('.rt-event') 223 | .last() 224 | .html() 225 | .indexOf('FIRST') 226 | ).to.be.above(-1); 227 | }); 228 | }); 229 | 230 | describe('Custom Components', () => { 231 | it('renders custom components', () => { 232 | const CUSTOM_COMPONENTS = { 233 | topLabel: CustomTopLabel, 234 | bottomLabel: CustomBottomLabel, 235 | header: CustomHeader, 236 | imageBody: CustomImageBody, 237 | textBody: CustomTextBody, 238 | footer: CustomFooter, 239 | }; 240 | const wrapper = render(); 241 | 242 | const firstLabelHtml = wrapper 243 | .find('.rt-label-container') 244 | .first() 245 | .html(); 246 | expect(firstLabelHtml.indexOf('*CustomTopLabel*')).to.be.above(-1); 247 | 248 | const lastLabelHtml = wrapper 249 | .find('.rt-label-container') 250 | .last() 251 | .html(); 252 | expect(lastLabelHtml.indexOf('*CustomBottomLabel*')).to.be.above(-1); 253 | 254 | const eventHtml = wrapper 255 | .find('.rt-event') 256 | .first() 257 | .html(); 258 | expect(eventHtml.indexOf('*CustomHeader*')).to.be.above(-1); 259 | expect(eventHtml.indexOf('*CustomImageBody*')).to.be.above(-1); 260 | expect(eventHtml.indexOf('*CustomTextBody*')).to.be.above(-1); 261 | expect(eventHtml.indexOf('*CustomFooter*')).to.be.above(-1); 262 | }); 263 | }); 264 | }); 265 | -------------------------------------------------------------------------------- /src/lib/timeline.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface TimelineEventClickHandler { 4 | (event: any): void; 5 | } 6 | 7 | export interface TimelineEvent { 8 | date: Date; 9 | title: string; 10 | imageUrl: string; 11 | text: string; 12 | onClick?: TimelineEventClickHandler | null; 13 | buttonText?: string | null; 14 | extras?: object | null; 15 | } 16 | 17 | export interface TimelineEventProps { 18 | event: TimelineEvent; 19 | } 20 | 21 | export interface TimelineCustomComponents { 22 | topLabel?: React.PureComponent | React.ReactNode | null; 23 | bottomLabel?: React.PureComponent | React.ReactNode | null; 24 | header?: React.PureComponent | React.ReactNode | null; 25 | imageBody?: React.PureComponent | React.ReactNode | null; 26 | textBody?: React.PureComponent | React.ReactNode | null; 27 | footer?: React.PureComponent | React.ReactNode | null; 28 | } 29 | 30 | export interface TimelineProps { 31 | customComponents?: TimelineCustomComponents | null; 32 | events: Array; 33 | reverseOrder?: boolean; 34 | denseLayout?: boolean; 35 | } 36 | 37 | const isNonZeroArray = (a: Array) => Array.isArray(a) && a.length > 0; 38 | 39 | const takeFirst = (a: Array) => (isNonZeroArray(a) ? a[0] : ({} as TimelineEvent)); 40 | 41 | const takeLast = (a: Array) => (isNonZeroArray(a) ? a[a.length - 1] : ({} as TimelineEvent)); 42 | 43 | const isValidDate = (date: Date) => { 44 | return date && date instanceof Date && !isNaN(date.getTime()); 45 | }; 46 | 47 | const formattedYear = (date: Date) => { 48 | return isValidDate(date) ? String(date.getFullYear()) : '-'; 49 | }; 50 | 51 | const formattedDate = (date: Date) => { 52 | if (!isValidDate(date)) return '-'; 53 | const day = String(date.getDate()); 54 | const month = String(date.getMonth() + 1); 55 | const year = String(date.getFullYear()); 56 | return `${month.length > 1 ? month : '0' + month}/${day.length > 1 ? day : '0' + day}/${year}`; 57 | }; 58 | 59 | const Dot = React.memo(function Dot(props) { 60 | return ( 61 | 62 | 63 | 64 | ); 65 | }); 66 | 67 | const Arrow = React.memo(function Arrow(props) { 68 | return ( 69 | 70 | 71 | 72 | 73 | 74 | ); 75 | }); 76 | 77 | const DefaultTopLabel = React.memo(function DefaultTopLabel(props: TimelineEventProps) { 78 | return
    {formattedYear(props.event.date)}
    ; 79 | }); 80 | 81 | const DefaultBottomLabel = React.memo(function DefaultBottomLabel(props: TimelineEventProps) { 82 | return
    {formattedYear(props.event.date)}
    ; 83 | }); 84 | 85 | const DefaultHeader = React.memo(function DefaultHeader(props: TimelineEventProps) { 86 | return ( 87 |
    88 |

    {props.event.title}

    89 |

    {formattedDate(props.event.date)}

    90 |
    91 | ); 92 | }); 93 | 94 | const DefaultFooter = React.memo(function DefaultFooter(props: TimelineEventProps) { 95 | const handleClick = (e: React.MouseEvent) => { 96 | e.preventDefault(); 97 | (props.event.onClick || (x => x))(e); 98 | }; 99 | 100 | return ( 101 | 104 | ); 105 | }); 106 | 107 | const DefaultTextBody = React.memo(function DefaultTextBody(props: TimelineEventProps) { 108 | return ( 109 |
    110 |

    {props.event.text}

    111 |
    112 | ); 113 | }); 114 | 115 | const DefaultImageBody = React.memo((props: TimelineEventProps) => { 116 | return ( 117 |
    118 | 119 |
    120 | ); 121 | }); 122 | 123 | const ArrowAndDot = React.memo(function ArrowAndDot(props) { 124 | return ( 125 |
    126 | 127 | 128 |
    129 | ); 130 | }); 131 | 132 | const Clear = React.memo(function Clear(props) { 133 | return
  • ; 134 | }); 135 | 136 | const Timeline = React.memo((props: TimelineProps) => { 137 | const { events, customComponents, reverseOrder, denseLayout } = props; 138 | 139 | const sortedEvents = events 140 | .slice(0) 141 | .filter(({ date }) => isValidDate(date)) 142 | .sort((a, b) => { 143 | return reverseOrder 144 | ? new Date(b.date).getTime() - new Date(a.date).getTime() 145 | : new Date(a.date).getTime() - new Date(b.date).getTime(); 146 | }); 147 | 148 | if (!sortedEvents.length) { 149 | return
    ; 150 | } 151 | 152 | const { topLabel, bottomLabel, header, footer, imageBody, textBody } = customComponents || {}; 153 | const TopComponent = (topLabel || DefaultTopLabel) as React.ComponentType; 154 | const BottomComponent = (bottomLabel || DefaultBottomLabel) as React.ComponentType; 155 | const HeaderComponent = (header || DefaultHeader) as React.ComponentType; 156 | const ImageBodyComponent = (imageBody || DefaultImageBody) as React.ComponentType; 157 | const TextBodyComponent = (textBody || DefaultTextBody) as React.ComponentType; 158 | const FooterComponent = (footer || DefaultFooter) as React.ComponentType; 159 | const eventStyles = denseLayout ? { minHeight: 'auto' } : {}; 160 | return
    161 |
    162 |
      163 |
    • 164 | 165 |
    • 166 | {sortedEvents.map((event, index) => { 167 | return ( 168 |
    • 169 |
      170 | 171 |
      172 |
      173 | 174 |
      175 |
      176 | 177 |
      178 |
      179 | 180 |
      181 |
      182 | 183 |
      184 |
      185 |
      186 |
    • 187 | ); 188 | })} 189 | 190 |
    • 191 | 192 |
    • 193 |
    194 |
    195 |
    ; 196 | }); 197 | 198 | export default Timeline; 199 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require('enzyme'); 2 | const EnzymeAdapter = require('enzyme-adapter-react-16'); 3 | 4 | Enzyme.configure({ adapter: new EnzymeAdapter() }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "es6", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "noEmit": false, 20 | "jsx": "react", 21 | "isolatedModules": false 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.sample: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "es6", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "noEmit": false, 20 | "jsx": "react", 21 | "isolatedModules": false 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: "./src/lib/timeline.tsx", 5 | output: { 6 | path: path.resolve(__dirname, "dist"), 7 | filename: "timeline.js", 8 | library: 'react-image-timeline', 9 | libraryTarget: 'commonjs2' 10 | }, 11 | mode: "production", 12 | resolve: { 13 | extensions: [".ts", ".tsx"] 14 | }, 15 | module: { 16 | rules: [ 17 | { test: /\.scss$/, use: "sass-loader" }, 18 | { test: /\.js$/, use: "babel-loader", exclude: /node_modules/ }, 19 | { test: /\.svg$/, use: "svg-inline-loader" }, 20 | { 21 | test: /\.tsx?$/, 22 | exclude: /node_modules/, 23 | use: [ 24 | { 25 | loader: "ts-loader" 26 | } 27 | ] 28 | } 29 | ] 30 | }, 31 | externals: { 32 | "react": "react" 33 | } 34 | }; 35 | --------------------------------------------------------------------------------