├── .babelrc
├── .eslintrc
├── .flowconfig
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── package.json
├── renovate.json
└── source
├── index.js
├── read-blob-file.js
└── spinner.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "es2016", "stage-0", "react"],
3 | "plugins": [
4 | "transform-merge-sibling-variables",
5 | "transform-minify-booleans",
6 | "transform-node-env-inline",
7 | "transform-react-constant-elements",
8 | "transform-react-inline-elements",
9 | "transform-remove-console",
10 | "transform-remove-debugger",
11 | "transform-simplify-comparison-operators",
12 | "transform-undefined-to-void"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "parser": "babel-eslint",
4 | "plugins": [
5 | "import",
6 | "jsx-a11y",
7 | "react",
8 | "flowtype",
9 | ],
10 | "env": {
11 | "browser": true,
12 | "node": true,
13 | },
14 | "parserOptions": {
15 | "ecmaFeatures": {
16 | "jsx": true,
17 | "es6": true,
18 | "classes": true,
19 | },
20 | },
21 | "settings": {
22 | "react": {
23 | "pragma": "React",
24 | "version": "15.3",
25 | },
26 | "flowtype": {
27 | "onlyFilesWithFlowAnnotation": false,
28 | },
29 | },
30 | "rules": {
31 | "flowtype/require-parameter-type": 1,
32 | "flowtype/require-return-type": [
33 | 1,
34 | "always",
35 | {
36 | "annotateUndefined": "never"
37 | },
38 | ],
39 | "flowtype/space-after-type-colon": [
40 | 1,
41 | "always",
42 | ],
43 | "flowtype/space-before-type-colon": [
44 | 1,
45 | "never",
46 | ],
47 | "flowtype/type-id-match": [
48 | 1,
49 | "^([A-Z][a-z0-9]+)+Type$",
50 | ],
51 | },
52 | }
53 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [include]
2 | source/
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 |
39 | build
40 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | source
3 | .babelrc
4 | .eslintrc
5 | .gitignore
6 | logs
7 | *.log
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Sergio Daniel Xalambrí
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 | # react-lazy-image
2 |
3 | Component to render images and lazyload them if are in the viewport (or near to them).
4 |
5 | This component extends from `React.PureComponent` so it needs React.js v15.3.0 or superior.
6 |
7 |
8 |
9 |
10 |
11 | ## Installation
12 | ```bash
13 | npm i -S react-lazy-image
14 | ```
15 |
16 | ## Usage example
17 | ```jsx
18 | import Image from 'react-lazy-image';
19 |
20 | const image = ;
21 | ```
22 |
23 | ## API
24 |
25 | ### `onLayout({ element: Object }): void`
26 | Called everytime the component is rendered or updated. Default: `() => {}`.
27 |
28 | ### `onError({ element: Object, error: Error }): void`
29 | Called if the request to load the image failed. Default: `() => {}`.
30 |
31 | ### `onLoad({ element: Object }): void`
32 | Called after the load ended (either successfully or not). Default: `() => {}`.
33 |
34 | ### `onLoadEnd({ element: Object }): void`
35 | Called after the load ended successfully. Default: `() => {}`.
36 |
37 | ### `onLoadStart({ element: Object }): void`
38 | Called when the request started. Default: `() => {}`.
39 |
40 | ### `onAbort({ element: Object }): void`
41 | Called if the load of the image was aborted. Default: `() => {}`.
42 |
43 | ### `onProgress({ element: Object }): void`
44 | Called everytime the AJAX progress event is dispatched. Default: `() => {}`.
45 |
46 | ### `offset: ?number`
47 | Set the amount of pixel near the viewport the component should be to start the image load. Default: `0`.
48 |
49 | ### `source: string`
50 | The image source path to load.
51 |
52 | ### `defaultSource: ?string`
53 | The default image source path or base64. If isn't defined then it uses a SVG animated spinner.
54 |
55 | ### `type: ?string`
56 | The format type of the image (`png`, `svg+xml`, `jpg` or `gif`). Default: `*`.
57 |
58 | ### `minLoaded: ?number`
59 | The minimum download percentaje to avoid aborting the request if the image leaves the viewport. Default: `50`.
60 |
61 | ## Common `img` attributes
62 | This component allow the usage of common `img` attributes like `alt`, `width`, `className`, etc. So you can use it as a normal `img` tag, just change `src` to `source` and (if you want to) add the other optional props.
63 |
64 | ## License
65 | The MIT License (MIT)
66 |
67 | Copyright (c) 2015 Sergio Daniel Xalambrí
68 |
69 | Permission is hereby granted, free of charge, to any person obtaining a copy
70 | of this software and associated documentation files (the "Software"), to deal
71 | in the Software without restriction, including without limitation the rights
72 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
73 | copies of the Software, and to permit persons to whom the Software is
74 | furnished to do so, subject to the following conditions:
75 |
76 | The above copyright notice and this permission notice shall be included in all
77 | copies or substantial portions of the Software.
78 |
79 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
80 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
81 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
82 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
83 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
84 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
85 | SOFTWARE.
86 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-lazy-image",
3 | "version": "1.1.0",
4 | "description": "Component to render images and lazyload them if are in the viewport (or near to them).",
5 | "main": "build/index.js",
6 | "scripts": {
7 | "lint": "eslint lib/index.js",
8 | "prebuild": "npm run lint",
9 | "build": "babel source -d build",
10 | "prepublish": "npm run build"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/sergiodxa/react-lazy-image.git"
15 | },
16 | "keywords": [
17 | "react",
18 | "lazyload",
19 | "image",
20 | "ajax"
21 | ],
22 | "author": "Sergio Daniel Xalambrí (http://sergio.xalambri.com.ar/)",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/sergiodxa/react-lazy-image/issues"
26 | },
27 | "homepage": "https://github.com/sergiodxa/react-lazy-image#readme",
28 | "peerDependencies": {
29 | "react": "^15.0.0"
30 | },
31 | "devDependencies": {
32 | "babel": "6.23.0",
33 | "babel-cli": "6.26.0",
34 | "babel-eslint": "6.1.2",
35 | "babel-plugin-transform-merge-sibling-variables": "6.8.0",
36 | "babel-plugin-transform-minify-booleans": "6.9.4",
37 | "babel-plugin-transform-node-env-inline": "6.8.0",
38 | "babel-plugin-transform-react-constant-elements": "6.23.0",
39 | "babel-plugin-transform-react-inline-elements": "6.22.0",
40 | "babel-plugin-transform-remove-console": "6.8.0",
41 | "babel-plugin-transform-remove-debugger": "6.9.4",
42 | "babel-plugin-transform-simplify-comparison-operators": "6.8.0",
43 | "babel-plugin-transform-undefined-to-void": "6.9.4",
44 | "babel-preset-es2015": "6.24.1",
45 | "babel-preset-es2016": "6.24.1",
46 | "babel-preset-react": "6.24.1",
47 | "babel-preset-stage-0": "6.24.1",
48 | "eslint": "3.2.2",
49 | "eslint-config-airbnb": "10.0.1",
50 | "eslint-plugin-flowtype": "2.4.0",
51 | "eslint-plugin-import": "1.16.0",
52 | "eslint-plugin-jsx-a11y": "2.0.1",
53 | "eslint-plugin-react": "6.0.0"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/source/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import defaultSource from './spinner.js';
4 | import readBlobFile from './read-blob-file.js';
5 |
6 | type RectType = {
7 | bottom: number,
8 | height: number,
9 | left: number,
10 | right: number,
11 | top: number,
12 | width: number,
13 | };
14 |
15 | type PropType = {
16 | onLayout(event: { element: Object }): void,
17 | onError(event: { element: Object, error: Error }): void,
18 | onLoad(event: { element: Object }): void,
19 | onLoadEnd(event: { element: Object }): void,
20 | onLoadStart(event: { element: Object }): void,
21 | onAbort(event: { element: Object }): void,
22 | onProgress(event: { element: Object }): void,
23 | offset: ?number,
24 | source: string,
25 | defaultSource: ?string,
26 | type: ?string,
27 | minLoaded: ?number,
28 | };
29 |
30 | /**
31 | * Instance the default props
32 | * @type {PropTypes}
33 | */
34 | const defaultProps: PropType = {
35 | onLayout: () => {},
36 | onError: () => {},
37 | onLoad: () => {},
38 | onLoadEnd: () => {},
39 | onLoadStart: () => {},
40 | onAbort: () => {},
41 | onProgress: () => {},
42 | offset: 0,
43 | defaultSource,
44 | type: '*',
45 | minLoaded: 50,
46 | };
47 |
48 | /**
49 | * Component to render an image using LazyLoad to request it only if the component is in
50 | * the viewport and abort the load if the component leaves the viewport
51 | */
52 | class Image extends Component {
53 | static defaultProps = defaultProps;
54 |
55 | /**
56 | * Bind component methods to `this`
57 | * @param {PropType} props [description]
58 | */
59 | constructor(props: PropType) {
60 | super(props);
61 | // bind function methods
62 | this.checkViewport = this.checkViewport.bind(this);
63 | this.setRef = this.setRef.bind(this);
64 |
65 | this.handleLoadEnd = this.handleLoadEnd.bind(this);
66 | this.handleAbort = this.handleAbort.bind(this);
67 | this.handleProgress = this.handleProgress.bind(this);
68 | this.handleError = this.handleError.bind(this);
69 | this.handleLoadStart = this.handleLoadStart.bind(this);
70 | this.handleLoad = this.handleLoad.bind(this);
71 | }
72 |
73 |
74 | /**
75 | * The state contains the image base64 string to use
76 | * @type {Object}
77 | */
78 | state: { image: string } = {
79 | image: this.props.defaultSource,
80 | };
81 |
82 |
83 | /**
84 | * Call the onLayout after the component is renderer
85 | * Create the XHR object
86 | * Set the event listener for the scroll event to check if the component is in viewport
87 | * Check the viewport one time to now if it's already in it
88 | */
89 | componentDidMount() {
90 | this.props.onLayout({ element: this });
91 |
92 | this.request = new XMLHttpRequest();
93 | this.request.responseType = 'arraybuffer';
94 |
95 | window.addEventListener('scroll', this.checkViewport);
96 | this.checkViewport();
97 | }
98 |
99 |
100 | /**
101 | * Call the onLayout callback if the component is re-rendered
102 | */
103 | componentDidUpdate() {
104 | this.props.onLayout({ element: this });
105 | }
106 |
107 |
108 | /**
109 | * Remove scroll event listener if the component is unmounted
110 | */
111 | componentWillUnmount() {
112 | window.removeEventListener('scroll', this.checkViewport);
113 | this.request.abort();
114 | }
115 |
116 |
117 | /**
118 | * Get the reference to the image
119 | * @param {Element} element The DOM element to set the reference
120 | */
121 | setRef(element: Element): Object {
122 | this.element = element;
123 | return this;
124 | }
125 |
126 |
127 | /**
128 | * Fetch the image and save it in the state
129 | */
130 | fetch(): void {
131 | // set request event handlers
132 | this.request.onloadstart = this.handleLoadStart;
133 | this.request.onprogress = this.handleProgress;
134 | this.request.onload = this.handleLoad;
135 | this.request.onloadend = this.handleLoadEnd;
136 | this.request.onabort = this.handleAbort;
137 | this.request.onerror = this.handleError;
138 |
139 | // open AJAX request
140 | this.request.open('GET', this.props.source);
141 | // send request
142 | return this.request.send();
143 | }
144 |
145 |
146 | /**
147 | * Handle load start event
148 | * @param {Object} event Request start event object
149 | */
150 | handleLoadStart(event: Object): void {
151 | this.isRequesting = true;
152 | const element = this;
153 |
154 | if (event.lengthComputable) {
155 | this.progress.loaded = 0;
156 | this.progress.total = event.total;
157 | }
158 |
159 | return this.props.onLoadStart({ element });
160 | }
161 |
162 |
163 | /**
164 | * Set the loaded progress (and the total)
165 | * @param {Object} event The progress event data
166 | */
167 | handleProgress(event: Object): void {
168 | const element = this;
169 |
170 | if (event.lengthComputable) {
171 | this.progress.loaded = event.loaded;
172 | }
173 |
174 | return this.props.onProgress({ element });
175 | }
176 |
177 |
178 | /**
179 | * Handle the XHR load event
180 | * @param {Object} event The load event data
181 | */
182 | handleLoad(): void {
183 | const element: Image = this;
184 | this.isRequesting = false;
185 | return this.props.onLoad({ element });
186 | }
187 |
188 |
189 | /**
190 | * If the request ended successful get the response as a blob object, transform it to base64,
191 | * remove the scroll event listener and update the `state.image` value`
192 | * @param {Object} event The load end event data
193 | */
194 | handleLoadEnd(): void {
195 | const element: Image = this;
196 |
197 | // if the request status es between 200 and 300
198 | if (this.request.status >= 200 && this.request.status < 300) {
199 | // transform response to a blob
200 | const blob: Blob = new Blob([this.request.response], { type: `image/${this.props.type}` });
201 | // read blob as a base64 string
202 | return readBlobFile(blob)
203 | .then((image: string) => {
204 | // set the image base64 string in the state
205 | this.setState({ image }, (): void => {
206 | // remove event scroll listener
207 | window.removeEventListener('scroll', this.checkViewport);
208 | // set the component as not requesting anymore
209 | this.isRequesting = false;
210 | // call the `onLoadEnd` callback
211 | return this.props.onLoadEnd({ element });
212 | });
213 | })
214 | .catch((error: Object): void => {
215 | // set the component as not requesting anymore
216 | this.isRequesting = false;
217 | // if an error happens call the `onError` callback
218 | return this.props.onError({ error, element });
219 | });
220 | }
221 |
222 | return null;
223 | }
224 |
225 |
226 | /**
227 | * Handle request error event
228 | * @param {Object} event The error event data
229 | */
230 | handleError(event: Object): void {
231 | const element: Image = this;
232 | this.isRequesting = false;
233 | return this.props.onError({ element, error: new Error(event.response) });
234 | }
235 |
236 |
237 | /**
238 | * Handle the request abort event
239 | * @param {Object} event The abort event data
240 | */
241 | handleAbort(): void {
242 | const element = this;
243 | this.isRequesting = false;
244 | return this.props.onAbort({ element });
245 | }
246 |
247 |
248 | /**
249 | * Check if the component is in the current viewport and load the image
250 | */
251 | checkViewport(): Promise | null {
252 | if (this.isInViewport && !this.isRequesting) {
253 | // if is in viewport and is not requesting start fetching
254 | return this.fetch();
255 | }
256 | if (!this.isInViewport && this.isRequesting) {
257 | // if isn't in viewport and is requesting
258 | if (this.amountLoaded < this.props.minLoaded || isNaN(this.amountLoaded)) {
259 | // if the amount loaded is lower than the `this.props.minLoaded`
260 | // or is NaN abort the request
261 | return this.request.abort();
262 | }
263 | }
264 | return null;
265 | }
266 |
267 |
268 | /**
269 | * Define the prop types
270 | * @type {PropType}
271 | */
272 | props: PropType;
273 |
274 |
275 | /**
276 | * If the component is requesting an image or not
277 | * @type {Boolean}
278 | */
279 | isRequesting: boolean = false;
280 |
281 |
282 | /**
283 | * Progress loaded and total amount of bytes
284 | * @type {Object}
285 | */
286 | progress: { loaded: number, total: number } = {
287 | loaded: 0,
288 | total: 1,
289 | };
290 |
291 |
292 | /**
293 | * The progress of amount lodaded
294 | * @return {Number} The percentaje loaded
295 | */
296 | get amountLoaded(): number {
297 | return (this.progress.loaded * 100) / this.progress.total;
298 | }
299 |
300 |
301 | /**
302 | * Check if the component is in the viewport
303 | * @return {Boolean} If the component is in viewport
304 | */
305 | get isInViewport(): boolean {
306 | // get element position in viewport
307 | const rect: RectType = this.element.getBoundingClientRect();
308 | // get viewport height and width
309 | const viewportHeight: number = (window.innerHeight || document.documentElement.clientHeight);
310 | const viewportWidth: number = (window.innerWidth || document.documentElement.clientWidth);
311 | // check if the element is in the viewport (or near to them)
312 | return (
313 | rect.bottom >= (0 - this.props.offset) &&
314 | rect.right >= (0 - this.props.offset) &&
315 | rect.top < (viewportHeight + this.props.offset) &&
316 | rect.left < (viewportWidth + this.props.offset)
317 | );
318 | }
319 |
320 |
321 | /**
322 | * Get the images props without the component own props
323 | * @return {Object} The filtered props
324 | */
325 | get imgProps(): Object {
326 | const ownProps = [
327 | 'onLayout',
328 | 'onError',
329 | 'onLoad',
330 | 'onLoadEnd',
331 | 'onLoadStart',
332 | 'onAbort',
333 | 'onProgress',
334 | 'resizeMode',
335 | 'source',
336 | 'defaultSource',
337 | 'offset',
338 | 'minLoaded',
339 | ];
340 |
341 | return Object
342 | .keys(this.props)
343 | .filter((propName: string): boolean => ownProps.indexOf(propName) === -1)
344 | .reduce(
345 | (props: PropType, propName: string): PropType => ({
346 | ...props,
347 | [propName]: this.props[propName],
348 | }),
349 | {},
350 | );
351 | }
352 |
353 |
354 | /**
355 | * Component renderer method
356 | * @return {Object} The image JSX element
357 | */
358 | render(): Object {
359 | return (
360 |
365 | );
366 | }
367 | }
368 |
369 | export default Image;
370 |
--------------------------------------------------------------------------------
/source/read-blob-file.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Read Blob file as a base64 string
3 | * @param {Blob} file The Blob file instance to read
4 | * @return {Promise} A reader promise
5 | */
6 | function readBlobFile(file: Blob): Promise {
7 | function promiseHandler(resolve: Function, reject: Function): FileReader {
8 | const reader: FileReader = new FileReader();
9 | reader.readAsDataURL(file);
10 | reader.onloadend = function onReadEnd(): void {
11 | if (reader.error) return reject(reader.error);
12 | return resolve(reader.result);
13 | };
14 | return reader;
15 | }
16 |
17 | return new Promise(promiseHandler);
18 | }
19 |
20 | export default readBlobFile;
21 |
--------------------------------------------------------------------------------
/source/spinner.js:
--------------------------------------------------------------------------------
1 | export default 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nNTBweCcgaGVpZ2h0PSc1MHB4JyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgY2xhc3M9InVpbC1kZWZhdWx0Ij48cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0ibm9uZSIgY2xhc3M9ImJrIj48L3JlY3Q+PHJlY3QgIHg9JzQ2JyB5PSc0MCcgd2lkdGg9JzgnIGhlaWdodD0nMjAnIHJ4PSc1JyByeT0nNScgZmlsbD0nI2MwYzBjMCcgdHJhbnNmb3JtPSdyb3RhdGUoMCA1MCA1MCkgdHJhbnNsYXRlKDAgLTMwKSc+ICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSdvcGFjaXR5JyBmcm9tPScxJyB0bz0nMCcgZHVyPScwLjdzJyBiZWdpbj0nMHMnIHJlcGVhdENvdW50PSdpbmRlZmluaXRlJy8+PC9yZWN0PjxyZWN0ICB4PSc0NicgeT0nNDAnIHdpZHRoPSc4JyBoZWlnaHQ9JzIwJyByeD0nNScgcnk9JzUnIGZpbGw9JyNjMGMwYzAnIHRyYW5zZm9ybT0ncm90YXRlKDQ1IDUwIDUwKSB0cmFuc2xhdGUoMCAtMzApJz4gIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9J29wYWNpdHknIGZyb209JzEnIHRvPScwJyBkdXI9JzAuN3MnIGJlZ2luPScwLjA4NzVzJyByZXBlYXRDb3VudD0naW5kZWZpbml0ZScvPjwvcmVjdD48cmVjdCAgeD0nNDYnIHk9JzQwJyB3aWR0aD0nOCcgaGVpZ2h0PScyMCcgcng9JzUnIHJ5PSc1JyBmaWxsPScjYzBjMGMwJyB0cmFuc2Zvcm09J3JvdGF0ZSg5MCA1MCA1MCkgdHJhbnNsYXRlKDAgLTMwKSc+ICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSdvcGFjaXR5JyBmcm9tPScxJyB0bz0nMCcgZHVyPScwLjdzJyBiZWdpbj0nMC4xNzVzJyByZXBlYXRDb3VudD0naW5kZWZpbml0ZScvPjwvcmVjdD48cmVjdCAgeD0nNDYnIHk9JzQwJyB3aWR0aD0nOCcgaGVpZ2h0PScyMCcgcng9JzUnIHJ5PSc1JyBmaWxsPScjYzBjMGMwJyB0cmFuc2Zvcm09J3JvdGF0ZSgxMzUgNTAgNTApIHRyYW5zbGF0ZSgwIC0zMCknPiAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0nb3BhY2l0eScgZnJvbT0nMScgdG89JzAnIGR1cj0nMC43cycgYmVnaW49JzAuMjYyNDk5OTk5OTk5OTk5OTZzJyByZXBlYXRDb3VudD0naW5kZWZpbml0ZScvPjwvcmVjdD48cmVjdCAgeD0nNDYnIHk9JzQwJyB3aWR0aD0nOCcgaGVpZ2h0PScyMCcgcng9JzUnIHJ5PSc1JyBmaWxsPScjYzBjMGMwJyB0cmFuc2Zvcm09J3JvdGF0ZSgxODAgNTAgNTApIHRyYW5zbGF0ZSgwIC0zMCknPiAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0nb3BhY2l0eScgZnJvbT0nMScgdG89JzAnIGR1cj0nMC43cycgYmVnaW49JzAuMzVzJyByZXBlYXRDb3VudD0naW5kZWZpbml0ZScvPjwvcmVjdD48cmVjdCAgeD0nNDYnIHk9JzQwJyB3aWR0aD0nOCcgaGVpZ2h0PScyMCcgcng9JzUnIHJ5PSc1JyBmaWxsPScjYzBjMGMwJyB0cmFuc2Zvcm09J3JvdGF0ZSgyMjUgNTAgNTApIHRyYW5zbGF0ZSgwIC0zMCknPiAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0nb3BhY2l0eScgZnJvbT0nMScgdG89JzAnIGR1cj0nMC43cycgYmVnaW49JzAuNDM3NXMnIHJlcGVhdENvdW50PSdpbmRlZmluaXRlJy8+PC9yZWN0PjxyZWN0ICB4PSc0NicgeT0nNDAnIHdpZHRoPSc4JyBoZWlnaHQ9JzIwJyByeD0nNScgcnk9JzUnIGZpbGw9JyNjMGMwYzAnIHRyYW5zZm9ybT0ncm90YXRlKDI3MCA1MCA1MCkgdHJhbnNsYXRlKDAgLTMwKSc+ICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSdvcGFjaXR5JyBmcm9tPScxJyB0bz0nMCcgZHVyPScwLjdzJyBiZWdpbj0nMC41MjQ5OTk5OTk5OTk5OTk5cycgcmVwZWF0Q291bnQ9J2luZGVmaW5pdGUnLz48L3JlY3Q+PHJlY3QgIHg9JzQ2JyB5PSc0MCcgd2lkdGg9JzgnIGhlaWdodD0nMjAnIHJ4PSc1JyByeT0nNScgZmlsbD0nI2MwYzBjMCcgdHJhbnNmb3JtPSdyb3RhdGUoMzE1IDUwIDUwKSB0cmFuc2xhdGUoMCAtMzApJz4gIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9J29wYWNpdHknIGZyb209JzEnIHRvPScwJyBkdXI9JzAuN3MnIGJlZ2luPScwLjYxMjQ5OTk5OTk5OTk5OTlzJyByZXBlYXRDb3VudD0naW5kZWZpbml0ZScvPjwvcmVjdD48L3N2Zz4=';
2 |
--------------------------------------------------------------------------------