├── .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 | [](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 | 
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 |
13 | );
14 | };
15 |
16 | const CustomBottomLabel = (props: TimelineEventProps) => {
17 | return (
18 |
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 |
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 |
64 | );
65 | });
66 |
67 | const Arrow = React.memo(function Arrow(props) {
68 | return (
69 |
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 |
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 |
--------------------------------------------------------------------------------