├── .babelrc ├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── example ├── .babelrc ├── README.md ├── build │ └── bundle.js ├── index.html ├── package.json ├── server.js ├── src │ ├── ReactExpandableGrid.css │ ├── index.js │ └── style.css └── webpack.config.js ├── index.html ├── package.json ├── react-expandable-grid.png ├── src └── index.js └── test └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | react-expandable-grid.png 3 | .babelrc 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.2" 4 | 5 | install: 6 | - npm install 7 | 8 | notifications: 9 | email: false 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Romain Berger 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-expandable-grid 2 | 3 | Image grid with an expanding detail view 4 | 5 | An Easy to use react component to get your galleries and portfolios up and running quickly. When an user clicks on an image thumbnail, a preview window opens up showing a larger (or smaller, depending on the settings) image along with some textual information such as a title or a description. 6 | 7 | It's always easier to use it than read about it... Check out the demo! 8 | 9 | 10 | ## Demo & Examples 11 | 12 | Live demo: [karthicashokan.github.io/react-expandable-grid](http://karthicashokan.github.io/react-expandable-grid/) 13 | 14 | To build the examples locally, run: 15 | 16 | ``` 17 | npm install 18 | npm run build 19 | cd example 20 | npm install 21 | npm start 22 | ``` 23 | 24 | Then open [`localhost:3000`](http://localhost:3000) in a browser. 25 | 26 | 27 | ## Installation 28 | 29 | The easiest way to use react-expandable-grid is to install it from NPM and include it in your own React build process. 30 | 31 | You can also use the standalone build by including `src/react-expandable-grid.js` in your page. If you use this, make sure you have already included React, and it is available as a global variable. 32 | 33 | ``` 34 | npm install react-expandable-grid --save 35 | ``` 36 | 37 | 38 | ## Usage 39 | 40 | 41 | ``` 42 | var ReactGridDetailExpansion = require('react-expandable-grid'); 43 | 44 | var data = [ 45 | {'img': '', 'link': '', 'title': '', 'description': ''}, 46 | {'img': '', 'link': '', 'title': '', 'description': ''} 47 | ] 48 | 49 | var data_string = JSON.stringify(data) 50 | 51 | 57 | ``` 58 | 59 | ### Properties 60 | 61 | | Property | Type | Explanation | Notes | 62 | | :--------------------------------- |:--------:| :----------------------------------------------------------------|--------:| 63 | | gridData | string | JSON string of the data to be displayed in the grid | | 64 | | cellSize | string | Size of the thumbnail image in the grid | | 65 | | cellMargin | number | Margin between the thumbnails in the grid | | 66 | | bgColor | string | Background color for the grid itself | | 67 | | detailWidth | string | Width of the expanded window | in % | 68 | | detailHeight | number | Height of the expanded window | in px| 69 | | detailBackgroundColor | string | Background color of the expanded window | | 70 | | ExpandedDetail_right_width | string | Width of the expanded window's right pane (see screenshot) | in % | 71 | | ExpandedDetail_left_width | string | Width of the expanded window's left pane (see screenshot) | in % | 72 | | ExpandedDetail_description_bgColor | string | Background color of the expanded window's description | | 73 | | ExpandedDetail_title_bgColor | string | Background color of the expanded window's title | | 74 | | ExpandedDetail_img_bgColor | string | Background color of the expanded window's image | | 75 | | ExpandedDetail_link_text | string | Text to be placed for the expanded window's link | | 76 | | ExpandedDetail_font_color | string | font-coloe of the expanded window's link | | 77 | | ExpandedDetail_close_x_bool | boolean | Whether you want the simple 'X' close button (else, use css) | | 78 | | show_mobile_style_from_width | number | Responsive design: Uses mobile optimization from this width | | 79 | 80 | 81 | ![Screen Shot](https://github.com/karthicashokan/react-expandable-grid/blob/master/react-expandable-grid.png) 82 | 83 | 84 | ### Notes 85 | 86 | ####Default values for properties are given below: 87 | 88 | ``` 89 | cellSize: 200, 90 | cellMargin: 25, 91 | bgColor: "#f2f2f2", 92 | detailWidth: "100%", 93 | detailHeight: 200, 94 | detailBackgroundColor: "#D9D9D9", 95 | ExpandedDetail_right_width: "60%", 96 | ExpandedDetail_left_width: "40%", 97 | ExpandedDetail_image_size: 200, 98 | ExpandedDetail_description_bgColor: "#D9D9D9", 99 | ExpandedDetail_title_bgColor: "#D9D9D9", 100 | ExpandedDetail_img_bgColor: "#D9D9D9", 101 | ExpandedDetail_link_text: "→ Link", 102 | ExpandedDetail_font_color: "#434343", 103 | ExpandedDetail_close_x_bool: false, // set false, if you're going to adding css to this div and making a customized button 104 | show_mobile_style_from_width: 600, 105 | ``` 106 | 107 | ####Use CSS to make elements look nicer. IDs for some key elements are given below: 108 | 109 | ``` 110 | // Link 111 | #ExpandedDetailDescriptionLink 112 | 113 | // Close Button 114 | #ExpandedDetail_close 115 | 116 | // Title for the expanded detail 117 | #ExpandedDetailTitle 118 | 119 | // Text/Description for the expanded detail 120 | #ExpandedDetailDescription 121 | 122 | // Image for the expanded detail 123 | #ExpandedDetailImage 124 | ``` 125 | 126 | ## Development (`src`, `lib` and the build process) 127 | 128 | **NOTE:** The source code for the component is in `src`. A transpiled CommonJS version (generated with Babel) is available in `lib` for use with node.js, browserify and webpack. A UMD bundle is also built to `dist`, which can be included without the need for any build system. 129 | 130 | To build, watch and serve the examples (which will also watch the component source), run `npm start`. If you just want to watch changes to `src` and rebuild `lib`, run `npm run watch` (this is useful if you are working with `npm link`). 131 | 132 | ## License 133 | 134 | ####(MIT License) 135 | 136 | ``` 137 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 138 | 139 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 140 | 141 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 142 | ``` 143 | 144 | Copyright (c) 2017 Karthic Ashokan. 145 | 146 | -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } 4 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # React Portal Tooltip 2 | 3 | ## Usage 4 | 5 | ```shell 6 | # install the dependencies 7 | $ npm install 8 | 9 | # run the development server with hot reloading 10 | $ npm start 11 | ``` 12 | 13 | Then open your browser at [http://localhost:3000](http://localhost:3000) 14 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | react-expandable-grid 6 | 7 | 8 | 9 | 10 |
11 |

react-expandable-grid

12 |

View project on GitHub

13 |
14 |
15 | 16 |
17 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-expandable-grid-example", 3 | "version": "1.0.2", 4 | "description": "", 5 | "main": "src/index.js", 6 | "author": "Karthic Ashokan ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "NODE_ENV=production BUILD=true webpack --progress --colors --display-error-details" 10 | }, 11 | "dependencies": { 12 | "babel-core": "^5.8.21", 13 | "babel-loader": "^5.3.2", 14 | "history": "2.0.1", 15 | "react": "^15.1.0", 16 | "react-dom": "^15.1.0", 17 | "react-image-placeholder": "^1.0.4", 18 | "react-router": "^1.0.3", 19 | "superagent": "^1.8.3", 20 | "webpack": "^1.11.0" 21 | }, 22 | "devDependencies": { 23 | "babel": "^5.8.21", 24 | "react-hot-loader": "^1.2.8", 25 | "webpack-dev-server": "^1.10.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'), 2 | WebpackDevServer = require('webpack-dev-server'), 3 | config = require('./webpack.config') 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true 9 | }).listen(3000, 'localhost', function (err, result) { 10 | if (err) { 11 | console.log(err) 12 | } 13 | 14 | console.log('Listening at localhost:3000') 15 | }) 16 | -------------------------------------------------------------------------------- /example/src/ReactExpandableGrid.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans'); 2 | 3 | /* -------------------------------------- To edit elements of this component -------------------------------------- */ 4 | 5 | /* Link */ 6 | #ExpandedDetailDescriptionLink { 7 | 8 | } 9 | 10 | /* Close Button */ 11 | #ExpandedDetail_close { 12 | content: ''; 13 | } 14 | 15 | /* Title for the expanded detail */ 16 | .ExpandedDetailTitle { 17 | display: block; 18 | font-size: 16px; 19 | margin-top: 10px; 20 | font-weight: bold; 21 | } 22 | 23 | /* Text/Description for the expanded detail */ 24 | .ExpandedDetailDescription { 25 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 26 | font-size: 14px; 27 | color: #333; 28 | } 29 | 30 | /* -------------------------------------- To make the links and close button look nice -------------------------------------- */ 31 | 32 | #ExpandedDetail_close { 33 | position: absolute; 34 | width: 25px; 35 | height: 25px; 36 | cursor: pointer; 37 | } 38 | 39 | #ExpandedDetail_close::before, 40 | #ExpandedDetail_close::after { 41 | content: ''; 42 | position: absolute; 43 | width: 100%; 44 | height: 1px; 45 | background: #888; 46 | -webkit-transform: rotate(45deg); 47 | -moz-transform: rotate(45deg); 48 | transform: rotate(45deg); 49 | } 50 | 51 | #ExpandedDetail_close::after { 52 | -webkit-transform: rotate(-45deg); 53 | -moz-transform: rotate(-45deg); 54 | transform: rotate(-45deg); 55 | } 56 | 57 | #ExpandedDetail_close:hover::before, 58 | #ExpandedDetail_close:hover::after { 59 | background: #333; 60 | } 61 | 62 | 63 | /* unvisited link */ 64 | #ExpandedDetailDescriptionLink { 65 | color: #008ed7; 66 | border: 2px solid #008ed7; 67 | padding: 5px; 68 | padding-left: 15px; 69 | padding-right: 15px; 70 | } 71 | 72 | /* visited link */ 73 | #ExpandedDetailDescriptionLink:visited { 74 | 75 | } 76 | 77 | /* mouse over link */ 78 | #ExpandedDetailDescriptionLink:hover { 79 | color: white; 80 | background: #008ed7; 81 | } 82 | 83 | /* selected link */ 84 | #ExpandedDetailDescriptionLink:active { 85 | color: white; 86 | background: #008ed7; 87 | border: 2px solid white; 88 | } -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import ReactExpandableGrid from './../../src' 4 | 5 | var data = [ 6 | {'img': 'http://i.imgur.com/zIEjP6Q.jpg', 'link': 'https://www.instagram.com/p/BRFjVZtgSJD/', 'title': 'Westland Tai Poutini National Park', 'description': 'Photo by @christopheviseux / The Westland Tai Poutini National Park in New Zealand’s South Island offers a remarkable opportunity to take a guided walk on a glacier. A helicopter drop high on the Franz Josef Glacier, provides access to explore stunning ice formations and blue ice caves. Follow me for more images around the world @christopheviseux #newzealand #mountain #ice'}, 7 | {'img': 'http://i.imgur.com/rCrvQTv.jpg', 'link': 'https://www.instagram.com/p/BQ6_Wa2gmdR/', 'title': 'Dubai Desert Conservation Reserve', 'description': 'Photo by @christopheviseux / Early morning flight on a hot air balloon ride above the Dubai Desert Conservation Reserve. Merely an hour drive from the city, the park was created to protect indigenous species and biodiversity. The Arabian Oryx, which was close to extinction, now has a population well over 100. There are many options to explore the desert and flying above may be one of the most mesmerizing ways. Follow me @christopheviseux for more images from the Middle East. #dubai #desert'}, 8 | {'img': 'http://i.imgur.com/U8iVzVl.jpg', 'link': 'https://www.instagram.com/p/BQyfDiKAEq9/', 'title': 'Crumbling Reflections', 'description': 'Photo @pedromcbride // Crumbling Reflections: Much has changed in Cuba over the 17 years I have visited this island. But much has stayed the same. Time still ticks at a Cuban pace and old cars still run… I don’t know how... and while pockets of new construction and renovation exist thanks to a growing tourism boom, most buildings are crumbling and cracking under the Caribbean climate. But amidst the hardship, nostalgia and messy vitality, the Cuban people keep moving, like their cars. And somehow, they do it with a colorful friendliness and warmth that always amazes me. To see more, follow @pedromcbride #cuba #havana #photo #workshop @natgeoexpeditions #reflection #photooftheday #petemcbride.'}, 9 | {'img': 'http://i.imgur.com/Ky9aJlE.jpg', 'link': 'https://www.instagram.com/p/BQxf6CEgD8p/', 'title': 'Impalas', 'description': 'Impetious young impala go head-to-head as they practice sparring. A talent they will need later in life when the rut begins. Photographed on assignment for @natgeotravel in Kruger National Park. For more images from Kruger, South Africa, follow @kengeiger #natgeotravel #krugernationalpark'}, 10 | {'img': 'http://i.imgur.com/mf3qfzt.jpg', 'link': 'https://www.instagram.com/p/BQvy7gbgynF/', 'title': 'Elephants', 'description': 'Photo by @ronan_donovan // Two bull African elephants at dawn in Uganda\'s Murchison Falls National Park. See more from Uganda with @ronan_donovan.'}, 11 | {'img': 'http://i.imgur.com/zIEjP6Q.jpg', 'link': 'https://www.instagram.com/p/BRFjVZtgSJD/', 'title': 'Westland Tai Poutini National Park', 'description': 'Photo by @christopheviseux / The Westland Tai Poutini National Park in New Zealand’s South Island offers a remarkable opportunity to take a guided walk on a glacier. A helicopter drop high on the Franz Josef Glacier, provides access to explore stunning ice formations and blue ice caves. Follow me for more images around the world @christopheviseux #newzealand #mountain #ice'}, 12 | {'img': 'http://i.imgur.com/rCrvQTv.jpg', 'link': 'https://www.instagram.com/p/BQ6_Wa2gmdR/', 'title': 'Dubai Desert Conservation Reserve', 'description': 'Photo by @christopheviseux / Early morning flight on a hot air balloon ride above the Dubai Desert Conservation Reserve. Merely an hour drive from the city, the park was created to protect indigenous species and biodiversity. The Arabian Oryx, which was close to extinction, now has a population well over 100. There are many options to explore the desert and flying above may be one of the most mesmerizing ways. Follow me @christopheviseux for more images from the Middle East. #dubai #desert'}, 13 | {'img': 'http://i.imgur.com/U8iVzVl.jpg', 'link': 'https://www.instagram.com/p/BQyfDiKAEq9/', 'title': 'Crumbling Reflections', 'description': 'Photo @pedromcbride // Crumbling Reflections: Much has changed in Cuba over the 17 years I have visited this island. But much has stayed the same. Time still ticks at a Cuban pace and old cars still run… I don’t know how... and while pockets of new construction and renovation exist thanks to a growing tourism boom, most buildings are crumbling and cracking under the Caribbean climate. But amidst the hardship, nostalgia and messy vitality, the Cuban people keep moving, like their cars. And somehow, they do it with a colorful friendliness and warmth that always amazes me. To see more, follow @pedromcbride #cuba #havana #photo #workshop @natgeoexpeditions #reflection #photooftheday #petemcbride.'}, 14 | {'img': 'http://i.imgur.com/Ky9aJlE.jpg', 'link': 'https://www.instagram.com/p/BQxf6CEgD8p/', 'title': 'Impalas', 'description': 'Impetious young impala go head-to-head as they practice sparring. A talent they will need later in life when the rut begins. Photographed on assignment for @natgeotravel in Kruger National Park. For more images from Kruger, South Africa, follow @kengeiger #natgeotravel #krugernationalpark'}, 15 | {'img': 'http://i.imgur.com/mf3qfzt.jpg', 'link': 'https://www.instagram.com/p/BQvy7gbgynF/', 'title': 'Elephants', 'description': 'Photo by @ronan_donovan // Two bull African elephants at dawn in Uganda\'s Murchison Falls National Park. See more from Uganda with @ronan_donovan.'}, 16 | {'img': 'http://i.imgur.com/zIEjP6Q.jpg', 'link': 'https://www.instagram.com/p/BRFjVZtgSJD/', 'title': 'Westland Tai Poutini National Park', 'description': 'Photo by @christopheviseux / The Westland Tai Poutini National Park in New Zealand’s South Island offers a remarkable opportunity to take a guided walk on a glacier. A helicopter drop high on the Franz Josef Glacier, provides access to explore stunning ice formations and blue ice caves. Follow me for more images around the world @christopheviseux #newzealand #mountain #ice'}, 17 | {'img': 'http://i.imgur.com/rCrvQTv.jpg', 'link': 'https://www.instagram.com/p/BQ6_Wa2gmdR/', 'title': 'Dubai Desert Conservation Reserve', 'description': 'Photo by @christopheviseux / Early morning flight on a hot air balloon ride above the Dubai Desert Conservation Reserve. Merely an hour drive from the city, the park was created to protect indigenous species and biodiversity. The Arabian Oryx, which was close to extinction, now has a population well over 100. There are many options to explore the desert and flying above may be one of the most mesmerizing ways. Follow me @christopheviseux for more images from the Middle East. #dubai #desert'}, 18 | {'img': 'http://i.imgur.com/zIEjP6Q.jpg', 'link': 'https://www.instagram.com/p/BRFjVZtgSJD/', 'title': 'Westland Tai Poutini National Park', 'description': 'Photo by @christopheviseux / The Westland Tai Poutini National Park in New Zealand’s South Island offers a remarkable opportunity to take a guided walk on a glacier. A helicopter drop high on the Franz Josef Glacier, provides access to explore stunning ice formations and blue ice caves. Follow me for more images around the world @christopheviseux #newzealand #mountain #ice'}, 19 | {'img': 'http://i.imgur.com/rCrvQTv.jpg', 'link': 'https://www.instagram.com/p/BQ6_Wa2gmdR/', 'title': 'Dubai Desert Conservation Reserve', 'description': 'Photo by @christopheviseux / Early morning flight on a hot air balloon ride above the Dubai Desert Conservation Reserve. Merely an hour drive from the city, the park was created to protect indigenous species and biodiversity. The Arabian Oryx, which was close to extinction, now has a population well over 100. There are many options to explore the desert and flying above may be one of the most mesmerizing ways. Follow me @christopheviseux for more images from the Middle East. #dubai #desert'}, 20 | {'img': 'http://i.imgur.com/U8iVzVl.jpg', 'link': 'https://www.instagram.com/p/BQyfDiKAEq9/', 'title': 'Crumbling Reflections', 'description': 'Photo @pedromcbride // Crumbling Reflections: Much has changed in Cuba over the 17 years I have visited this island. But much has stayed the same. Time still ticks at a Cuban pace and old cars still run… I don’t know how... and while pockets of new construction and renovation exist thanks to a growing tourism boom, most buildings are crumbling and cracking under the Caribbean climate. But amidst the hardship, nostalgia and messy vitality, the Cuban people keep moving, like their cars. And somehow, they do it with a colorful friendliness and warmth that always amazes me. To see more, follow @pedromcbride #cuba #havana #photo #workshop @natgeoexpeditions #reflection #photooftheday #petemcbride.'}, 21 | {'img': 'http://i.imgur.com/Ky9aJlE.jpg', 'link': 'https://www.instagram.com/p/BQxf6CEgD8p/', 'title': 'Impalas', 'description': 'Impetious young impala go head-to-head as they practice sparring. A talent they will need later in life when the rut begins. Photographed on assignment for @natgeotravel in Kruger National Park. For more images from Kruger, South Africa, follow @kengeiger #natgeotravel #krugernationalpark'}, 22 | {'img': 'http://i.imgur.com/mf3qfzt.jpg', 'link': 'https://www.instagram.com/p/BQvy7gbgynF/', 'title': 'Elephants', 'description': 'Photo by @ronan_donovan // Two bull African elephants at dawn in Uganda\'s Murchison Falls National Park. See more from Uganda with @ronan_donovan.'}, 23 | {'img': 'http://i.imgur.com/zIEjP6Q.jpg', 'link': 'https://www.instagram.com/p/BRFjVZtgSJD/', 'title': 'Westland Tai Poutini National Park', 'description': 'Photo by @christopheviseux / The Westland Tai Poutini National Park in New Zealand’s South Island offers a remarkable opportunity to take a guided walk on a glacier. A helicopter drop high on the Franz Josef Glacier, provides access to explore stunning ice formations and blue ice caves. Follow me for more images around the world @christopheviseux #newzealand #mountain #ice'}, 24 | {'img': 'http://i.imgur.com/rCrvQTv.jpg', 'link': 'https://www.instagram.com/p/BQ6_Wa2gmdR/', 'title': 'Dubai Desert Conservation Reserve', 'description': 'Photo by @christopheviseux / Early morning flight on a hot air balloon ride above the Dubai Desert Conservation Reserve. Merely an hour drive from the city, the park was created to protect indigenous species and biodiversity. The Arabian Oryx, which was close to extinction, now has a population well over 100. There are many options to explore the desert and flying above may be one of the most mesmerizing ways. Follow me @christopheviseux for more images from the Middle East. #dubai #desert'}, 25 | {'img': 'http://i.imgur.com/U8iVzVl.jpg', 'link': 'https://www.instagram.com/p/BQyfDiKAEq9/', 'title': 'Crumbling Reflections', 'description': 'Photo @pedromcbride // Crumbling Reflections: Much has changed in Cuba over the 17 years I have visited this island. But much has stayed the same. Time still ticks at a Cuban pace and old cars still run… I don’t know how... and while pockets of new construction and renovation exist thanks to a growing tourism boom, most buildings are crumbling and cracking under the Caribbean climate. But amidst the hardship, nostalgia and messy vitality, the Cuban people keep moving, like their cars. And somehow, they do it with a colorful friendliness and warmth that always amazes me. To see more, follow @pedromcbride #cuba #havana #photo #workshop @natgeoexpeditions #reflection #photooftheday #petemcbride.'}, 26 | {'img': 'http://i.imgur.com/Ky9aJlE.jpg', 'link': 'https://www.instagram.com/p/BQxf6CEgD8p/', 'title': 'Impalas', 'description': 'Impetious young impala go head-to-head as they practice sparring. A talent they will need later in life when the rut begins. Photographed on assignment for @natgeotravel in Kruger National Park. For more images from Kruger, South Africa, follow @kengeiger #natgeotravel #krugernationalpark'}, 27 | {'img': 'http://i.imgur.com/mf3qfzt.jpg', 'link': 'https://www.instagram.com/p/BQvy7gbgynF/', 'title': 'Elephants', 'description': 'Photo by @ronan_donovan // Two bull African elephants at dawn in Uganda\'s Murchison Falls National Park. See more from Uganda with @ronan_donovan.'}, 28 | {'img': 'http://i.imgur.com/zIEjP6Q.jpg', 'link': 'https://www.instagram.com/p/BRFjVZtgSJD/', 'title': 'Westland Tai Poutini National Park', 'description': 'Photo by @christopheviseux / The Westland Tai Poutini National Park in New Zealand’s South Island offers a remarkable opportunity to take a guided walk on a glacier. A helicopter drop high on the Franz Josef Glacier, provides access to explore stunning ice formations and blue ice caves. Follow me for more images around the world @christopheviseux #newzealand #mountain #ice'}, 29 | {'img': 'http://i.imgur.com/rCrvQTv.jpg', 'link': 'https://www.instagram.com/p/BQ6_Wa2gmdR/', 'title': 'Dubai Desert Conservation Reserve', 'description': 'Photo by @christopheviseux / Early morning flight on a hot air balloon ride above the Dubai Desert Conservation Reserve. Merely an hour drive from the city, the park was created to protect indigenous species and biodiversity. The Arabian Oryx, which was close to extinction, now has a population well over 100. There are many options to explore the desert and flying above may be one of the most mesmerizing ways. Follow me @christopheviseux for more images from the Middle East. #dubai #desert'} 30 | ] 31 | 32 | var dataString = JSON.stringify(data) 33 | 34 | document.addEventListener('DOMContentLoaded', () => { 35 | ReactDOM.render( 36 | , 43 | document.querySelector('#app') 44 | ) 45 | }) 46 | -------------------------------------------------------------------------------- /example/src/style.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 4 | font-size: 14px; 5 | color: #333; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | a { 11 | color: #08c; 12 | text-decoration: none; 13 | } 14 | 15 | a:hover { 16 | text-decoration: underline; 17 | } 18 | 19 | .container { 20 | margin-left: auto; 21 | margin-right: auto; 22 | max-width: 90%; 23 | padding: 1em; 24 | } 25 | 26 | .footer { 27 | margin-top: 50px; 28 | border-top: 1px solid #eee; 29 | padding: 20px 0; 30 | font-size: 12px; 31 | color: #999; 32 | } 33 | 34 | h1, h2, h3, h4, h5, h6 { 35 | color: #222; 36 | font-weight: 100; 37 | margin: 0.5em 0; 38 | } 39 | 40 | label { 41 | color: #999; 42 | display: inline-block; 43 | font-size: 0.85em; 44 | font-weight: bold; 45 | margin: 1em 0; 46 | text-transform: uppercase; 47 | } 48 | 49 | .hint { 50 | margin: 15px 0; 51 | font-style: italic; 52 | color: #999; 53 | } -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | webpack = require('webpack') 3 | 4 | var config = { 5 | entry: [ 6 | path.join(__dirname, 'src/index.js') 7 | ], 8 | output: { 9 | path: path.join(__dirname, 'build'), 10 | publicPath: '/build/', 11 | filename: 'bundle.js' 12 | }, 13 | module: { 14 | loaders: [ 15 | {test: /\.js$/, exclude: /node_modules/, loaders: ['react-hot', 'babel-loader?stage=0']} 16 | ] 17 | }, 18 | plugins: [ 19 | ], 20 | } 21 | 22 | if (!process.env.BUILD) { 23 | config.entry.push('webpack-dev-server/client?http://0.0.0.0:3000') 24 | config.entry.push('webpack/hot/only-dev-server') 25 | 26 | config.plugins = [ 27 | new webpack.HotModuleReplacementPlugin(), 28 | new webpack.NoErrorsPlugin() 29 | ] 30 | } 31 | 32 | module.exports = config 33 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | react-expandable-grid 6 | 7 | 8 | 9 | 10 |
11 |

react-expandable-grid

12 |

View project on GitHub

13 |
14 |
15 | 16 |
17 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-expandable-grid", 3 | "version": "1.0.3", 4 | "description": "Image grid with an expanding detail view", 5 | "main": "lib/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/karthicashokan/react-expandable-grid.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/karthicashokan/react-expandable-grid.git" 12 | }, 13 | "homepage": "https://github.com/karthicashokan/react-expandable-grid.git", 14 | "scripts": { 15 | "start": "./node_modules/.bin/babel src --watch --out-dir lib", 16 | "build": "./node_modules/.bin/babel src --out-dir lib", 17 | "test": "mocha --compilers js:babel/register", 18 | "prepublish": "rm -rf lib && npm run build" 19 | }, 20 | "keywords": [ 21 | "react", 22 | "gallery", 23 | "grid", 24 | "portfolio", 25 | "expandable", 26 | "preview", 27 | "thumbnail" 28 | ], 29 | "author": "Karthic Ashokan ", 30 | "license": "MIT", 31 | "peerDependencies": { 32 | "react": "^15.1.0", 33 | "react-dom": "^15.1.0" 34 | }, 35 | "dependencies": { 36 | "object-assign": "^4.0.1" 37 | }, 38 | "devDependencies": { 39 | "babel": "^5.8.21", 40 | "mocha": "^2.3.3", 41 | "react": "^15.1.0", 42 | "react-addons-test-utils": "^15.0.2", 43 | "react-dom": "^15.1.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /react-expandable-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karthicashokan/react-expandable-grid/df2f4858f1e7ad9cbcc9d3ee04bce1f69ddab847/react-expandable-grid.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint react/prop-types: 0 */ 2 | /* eslint react/jsx-no-bind: 0 */ 3 | 4 | import React from 'react' 5 | import ReactDOM from 'react-dom' 6 | 7 | class SingleGridCell extends React.Component { 8 | 9 | constructor (props) { 10 | super(props) 11 | 12 | this.state = { 13 | expanded: false, 14 | selected_id: '', 15 | window_width: window.innerWidth 16 | } 17 | } 18 | 19 | cellClick (event) { 20 | this.props.handleCellClick(event) 21 | } 22 | 23 | render () { 24 | var SingleGridCellStyle = { 25 | background: 'url(' + this.props.SingleGridCellData['img'] + ') no-repeat center center', 26 | backgroundSize: this.props.cellSize, 27 | width: this.props.cellSize, 28 | height: this.props.cellSize, 29 | display: 'inline-block', 30 | margin: this.props.cellMargin, 31 | marginBottom: 25, 32 | position: 'relative' 33 | } 34 | 35 | return ( 36 |
  • 37 | ) 38 | } 39 | } 40 | 41 | class ReactExpandableGrid extends React.Component { 42 | 43 | constructor (props) { 44 | super(props) 45 | 46 | this.state = { 47 | expanded: false, 48 | selected_id: '', 49 | gridData: JSON.parse(this.props.gridData) 50 | } 51 | } 52 | 53 | handleResize () { 54 | if (this.state.expanded) { 55 | var target = document.getElementById(this.state.selected_id) 56 | this.renderExpandedDetail(target) 57 | } 58 | this.makeItMobileFriendly() 59 | } 60 | 61 | makeItMobileFriendly () { 62 | var leftPanel = document.getElementById('ExpandedDetail_left') 63 | var rightPanel = document.getElementById('ExpandedDetail_right') 64 | if (window.innerWidth < this.props.show_mobile_style_from_width) { 65 | leftPanel.style.display = 'none' 66 | rightPanel.style.width = '100%' 67 | } else { 68 | leftPanel.style.display = 'block' 69 | leftPanel.style.width = this.props.ExpandedDetail_left_width 70 | rightPanel.style.width = this.props.ExpandedDetail_right_width 71 | } 72 | } 73 | 74 | componentWillMount () { 75 | window.addEventListener('resize', this.handleResize.bind(this)) 76 | } 77 | 78 | componentWillUnmount () { } 79 | 80 | renderExpandedDetail (target) { 81 | var thisId = target.id 82 | var thisIdNumber = parseInt(thisId.substring(10)) 83 | var detail = document.getElementById('expandedDetail') 84 | var ol = target.parentNode 85 | var lengthOfList = parseInt(ol.childNodes.length) 86 | var startingIndex = thisIdNumber + 1 87 | 88 | var insertedFlag = false 89 | 90 | ol.insertBefore(detail, ol.childNodes[lengthOfList]) 91 | 92 | for (var i = startingIndex; i < lengthOfList; i++) { 93 | if (ol.childNodes[i].className === 'SingleGridCell') { 94 | if (ol.childNodes[i].offsetTop !== ol.childNodes[thisIdNumber].offsetTop) { 95 | ol.childNodes[i].insertAdjacentElement('beforebegin', detail) 96 | insertedFlag = true 97 | break 98 | } 99 | } 100 | } 101 | 102 | if (insertedFlag === false) { 103 | ol.childNodes[lengthOfList - 1].insertAdjacentElement('afterend', detail) 104 | } 105 | 106 | var cell = document.getElementById(thisId) 107 | var arrow = document.getElementById('selected_arrow') 108 | cell.append(arrow) 109 | arrow.style.display = 'block' 110 | } 111 | 112 | closeExpandedDetail () { 113 | this.setState({ 114 | expanded: false, 115 | selected_id: '' 116 | }, function afterStateChange () { 117 | var detail = document.getElementById('expandedDetail') 118 | detail.style.display = 'none' 119 | var arrow = document.getElementById('selected_arrow') 120 | arrow.style.display = 'none' 121 | }) 122 | } 123 | 124 | handleCellClick (event) { 125 | var target = event.target 126 | var thisIdNumber = parseInt(event.target.id.substring(10)) 127 | 128 | if (this.state.expanded) { // expanded == true 129 | if (this.state.selected_id === event.target.id) { // Clicking on already opened detail 130 | this.closeExpandedDetail() 131 | this.renderExpandedDetail(target) 132 | } else { // Clicking on a different thumbnail, when detail is already expanded 133 | this.setState({ 134 | expanded: true, 135 | selected_id: event.target.id 136 | }, function afterStateChange () { 137 | var detail = document.getElementById('expandedDetail') 138 | var description = document.getElementById('ExpandedDetailDescription') 139 | var title = document.getElementById('ExpandedDetailTitle') 140 | var img = document.getElementById('ExpandedDetailImage') 141 | var DescriptionLink = document.getElementById('ExpandedDetailDescriptionLink') 142 | var ImageLink = document.getElementById('ExpandedDetailImageLink') 143 | description.innerHTML = this.state.gridData[thisIdNumber]['description'] 144 | title.innerHTML = this.state.gridData[thisIdNumber]['title'] 145 | img.src = this.state.gridData[thisIdNumber]['img'] 146 | DescriptionLink.href = this.state.gridData[thisIdNumber]['link'] 147 | ImageLink.href = this.state.gridData[thisIdNumber]['link'] 148 | 149 | this.renderExpandedDetail(target) 150 | 151 | detail.style.display = 'block' 152 | }) 153 | } 154 | } else { // expanded == false 155 | this.setState({ 156 | expanded: true, 157 | selected_id: event.target.id 158 | }, function afterStateChange () { 159 | var detail = document.getElementById('expandedDetail') 160 | var description = document.getElementById('ExpandedDetailDescription') 161 | var title = document.getElementById('ExpandedDetailTitle') 162 | var img = document.getElementById('ExpandedDetailImage') 163 | var DescriptionLink = document.getElementById('ExpandedDetailDescriptionLink') 164 | var ImageLink = document.getElementById('ExpandedDetailImageLink') 165 | description.innerHTML = this.state.gridData[thisIdNumber]['description'] 166 | title.innerHTML = this.state.gridData[thisIdNumber]['title'] 167 | img.src = this.state.gridData[thisIdNumber]['img'] 168 | DescriptionLink.href = this.state.gridData[thisIdNumber]['link'] 169 | ImageLink.href = this.state.gridData[thisIdNumber]['link'] 170 | 171 | this.renderExpandedDetail(target) 172 | 173 | detail.style.display = 'block' 174 | }) 175 | } 176 | } 177 | 178 | generateGrid () { 179 | var grid = [] 180 | var idCounter = -1 // To help simplify mapping to object array indices. For example,
  • with 0th id corresponds to 0th child of
      181 | var gridData = this.state.gridData 182 | 183 | for (var i in gridData) { 184 | idCounter = idCounter + 1 185 | var thisUniqueKey = 'grid_cell_' + idCounter.toString() 186 | grid.push() 187 | } 188 | 189 | var cssforExpandedDetail = { 190 | backgroundColor: this.props.detailBackgroundColor, 191 | height: this.props.detailHeight, 192 | display: 'none', 193 | position: 'relative', 194 | padding: '20px', 195 | transition: 'display 2s ease-in-out 0.5s' 196 | } 197 | 198 | var cssforExpandedDetailImage = { 199 | display: 'inline-block', 200 | maxWidth: this.props.ExpandedDetail_image_size, 201 | width: '100%', 202 | height: 'auto', 203 | align: 'center', 204 | position: 'absolute', 205 | top: 0, 206 | bottom: 0, 207 | left: 0, 208 | right: 0, 209 | margin: 'auto' 210 | } 211 | 212 | var cssforExpandedDetailTitle = { 213 | backgroundColor: this.props.ExpandedDetail_title_bgColor, 214 | width: '100%', 215 | height: 'auto', 216 | marginBottom: '15px' 217 | } 218 | 219 | var cssforExpandedDetailDescription = { 220 | backgroundColor: this.props.ExpandedDetail_description_bgColor, 221 | color: this.props.ExpandedDetail_font_color, 222 | width: 'auto%', 223 | height: '80%', 224 | marginRight: '30px', 225 | marginLeft: '30px', 226 | textAlign: 'justify' 227 | } 228 | 229 | var cssforExpandedDetailLeft 230 | var cssforExpandedDetailRight 231 | 232 | cssforExpandedDetailLeft = { 233 | width: this.props.ExpandedDetail_left_width, 234 | height: '100%', 235 | float: 'left', 236 | position: 'relative' 237 | } 238 | 239 | cssforExpandedDetailRight = { 240 | width: this.props.ExpandedDetail_right_width, 241 | height: '100%', 242 | float: 'right', 243 | position: 'relative' 244 | } 245 | 246 | var cssForDescriptionLink = { 247 | textDecoration: 'none', 248 | position: 'relative', 249 | float: 'bottom', 250 | bottom: 20, 251 | cursor: 'pointer' 252 | } 253 | 254 | var cssForImageLink = { 255 | cursor: 'pointer' 256 | } 257 | 258 | var cssforExpandedDetailClose = { 259 | textDecoration: 'none', 260 | position: 'relative', 261 | float: 'right', 262 | top: 10, 263 | right: 10, 264 | cursor: 'pointer' 265 | } 266 | 267 | // Make Mobile Friendly 268 | if (window.innerWidth < this.props.show_mobile_style_from_width) { 269 | cssforExpandedDetailLeft = { 270 | width: '0%', 271 | height: '100%', 272 | float: 'left', 273 | position: 'relative', 274 | display: 'none' 275 | } 276 | 277 | cssforExpandedDetailRight = { 278 | width: '100%', 279 | height: '100%', 280 | float: 'right', 281 | position: 'relative' 282 | } 283 | } 284 | 285 | var closeX 286 | if (this.props.ExpandedDetail_closeX_bool) { 287 | closeX = 'X' 288 | } else { 289 | closeX = '' 290 | } 291 | 292 | grid.push( 293 |
    1. 294 |
      295 | 296 | 297 | 298 |
      299 |
      300 |
      {closeX}
      301 |
      Title
      302 |
      Some Description
      303 | → Link 304 |
      305 |
    2. 306 | ) 307 | 308 | return grid 309 | } 310 | 311 | render () { 312 | var rows = this.generateGrid() 313 | 314 | var cssForGridDetailExpansion = { 315 | width: '100%', 316 | position: 'relative' 317 | } 318 | 319 | var cssForGridList = { 320 | listStyle: 'none', 321 | padding: 0, 322 | display: 'inline-block' 323 | } 324 | 325 | var cssForTheGridHolder = { 326 | width: '100%', 327 | backgroundColor: this.props.bgColor, 328 | margin: 0, 329 | textAlign: 'center' 330 | } 331 | 332 | var cssForSelectedArrow = { 333 | width: 0, 334 | height: 0, 335 | borderLeft: '20px solid transparent', 336 | borderRight: '20px solid transparent', 337 | borderBottom: '30px solid' + this.props.detailBackgroundColor, 338 | marginTop: this.props.cellSize, 339 | marginLeft: this.props.cellSize / 2 - 20, 340 | display: 'none' 341 | } 342 | 343 | return ( 344 |
      345 |
      346 |
        347 | {rows} 348 |
      349 |
      350 |
      351 |
      352 | ) 353 | } 354 | } 355 | 356 | ReactExpandableGrid.propTypes = { 357 | gridData: React.PropTypes.string, 358 | cellSize: React.PropTypes.number, 359 | cellMargin: React.PropTypes.number, 360 | bgColor: React.PropTypes.string, 361 | detailWidth: React.PropTypes.string, // in % 362 | detailHeight: React.PropTypes.number, 363 | detailBackgroundColor: React.PropTypes.string, 364 | ExpandedDetail_right_width: React.PropTypes.string, // in % 365 | ExpandedDetail_left_width: React.PropTypes.string, // in % 366 | ExpandedDetail_description_bgColor: React.PropTypes.string, 367 | ExpandedDetail_title_bgColor: React.PropTypes.string, 368 | ExpandedDetail_img_bgColor: React.PropTypes.string, 369 | ExpandedDetail_link_text: React.PropTypes.string, 370 | ExpandedDetail_font_color: React.PropTypes.string, 371 | ExpandedDetail_closeX_bool: React.PropTypes.bool, 372 | show_mobile_style_from_width: React.PropTypes.number 373 | } 374 | 375 | var data = [ 376 | {'img': 'http://i.imgur.com/zIEjP6Q.jpg', 'link': 'https://www.instagram.com/p/BRFjVZtgSJD/', 'title': 'Westland Tai Poutini National Park', 'description': 'Photo by @christopheviseux / The Westland Tai Poutini National Park in New Zealand’s South Island offers a remarkable opportunity to take a guided walk on a glacier. A helicopter drop high on the Franz Josef Glacier, provides access to explore stunning ice formations and blue ice caves. Follow me for more images around the world @christopheviseux #newzealand #mountain #ice'}, 377 | {'img': 'http://i.imgur.com/rCrvQTv.jpg', 'link': 'https://www.instagram.com/p/BQ6_Wa2gmdR/', 'title': 'Dubai Desert Conservation Reserve', 'description': 'Photo by @christopheviseux / Early morning flight on a hot air balloon ride above the Dubai Desert Conservation Reserve. Merely an hour drive from the city, the park was created to protect indigenous species and biodiversity. The Arabian Oryx, which was close to extinction, now has a population well over 100. There are many options to explore the desert and flying above may be one of the most mesmerizing ways. Follow me @christopheviseux for more images from the Middle East. #dubai #desert'}, 378 | {'img': 'http://i.imgur.com/U8iVzVl.jpg', 'link': 'https://www.instagram.com/p/BQyfDiKAEq9/', 'title': 'Crumbling Reflections', 'description': 'Photo @pedromcbride // Crumbling Reflections: Much has changed in Cuba over the 17 years I have visited this island. But much has stayed the same. Time still ticks at a Cuban pace and old cars still run… I don’t know how... and while pockets of new construction and renovation exist thanks to a growing tourism boom, most buildings are crumbling and cracking under the Caribbean climate. But amidst the hardship, nostalgia and messy vitality, the Cuban people keep moving, like their cars. And somehow, they do it with a colorful friendliness and warmth that always amazes me. To see more, follow @pedromcbride #cuba #havana #photo #workshop @natgeoexpeditions #reflection #photooftheday #petemcbride.'}, 379 | {'img': 'http://i.imgur.com/Ky9aJlE.jpg', 'link': 'https://www.instagram.com/p/BQxf6CEgD8p/', 'title': 'Impalas', 'description': 'Impetious young impala go head-to-head as they practice sparring. A talent they will need later in life when the rut begins. Photographed on assignment for @natgeotravel in Kruger National Park. For more images from Kruger, South Africa, follow @kengeiger #natgeotravel #krugernationalpark'}, 380 | {'img': 'http://i.imgur.com/mf3qfzt.jpg', 'link': 'https://www.instagram.com/p/BQvy7gbgynF/', 'title': 'Elephants', 'description': 'Photo by @ronan_donovan // Two bull African elephants at dawn in Uganda\'s Murchison Falls National Park. See more from Uganda with @ronan_donovan.'}, 381 | {'img': 'http://i.imgur.com/zIEjP6Q.jpg', 'link': 'https://www.instagram.com/p/BRFjVZtgSJD/', 'title': 'Westland Tai Poutini National Park', 'description': 'Photo by @christopheviseux / The Westland Tai Poutini National Park in New Zealand’s South Island offers a remarkable opportunity to take a guided walk on a glacier. A helicopter drop high on the Franz Josef Glacier, provides access to explore stunning ice formations and blue ice caves. Follow me for more images around the world @christopheviseux #newzealand #mountain #ice'}, 382 | {'img': 'http://i.imgur.com/rCrvQTv.jpg', 'link': 'https://www.instagram.com/p/BQ6_Wa2gmdR/', 'title': 'Dubai Desert Conservation Reserve', 'description': 'Photo by @christopheviseux / Early morning flight on a hot air balloon ride above the Dubai Desert Conservation Reserve. Merely an hour drive from the city, the park was created to protect indigenous species and biodiversity. The Arabian Oryx, which was close to extinction, now has a population well over 100. There are many options to explore the desert and flying above may be one of the most mesmerizing ways. Follow me @christopheviseux for more images from the Middle East. #dubai #desert'}, 383 | {'img': 'http://i.imgur.com/U8iVzVl.jpg', 'link': 'https://www.instagram.com/p/BQyfDiKAEq9/', 'title': 'Crumbling Reflections', 'description': 'Photo @pedromcbride // Crumbling Reflections: Much has changed in Cuba over the 17 years I have visited this island. But much has stayed the same. Time still ticks at a Cuban pace and old cars still run… I don’t know how... and while pockets of new construction and renovation exist thanks to a growing tourism boom, most buildings are crumbling and cracking under the Caribbean climate. But amidst the hardship, nostalgia and messy vitality, the Cuban people keep moving, like their cars. And somehow, they do it with a colorful friendliness and warmth that always amazes me. To see more, follow @pedromcbride #cuba #havana #photo #workshop @natgeoexpeditions #reflection #photooftheday #petemcbride.'}, 384 | {'img': 'http://i.imgur.com/Ky9aJlE.jpg', 'link': 'https://www.instagram.com/p/BQxf6CEgD8p/', 'title': 'Impalas', 'description': 'Impetious young impala go head-to-head as they practice sparring. A talent they will need later in life when the rut begins. Photographed on assignment for @natgeotravel in Kruger National Park. For more images from Kruger, South Africa, follow @kengeiger #natgeotravel #krugernationalpark'}, 385 | {'img': 'http://i.imgur.com/mf3qfzt.jpg', 'link': 'https://www.instagram.com/p/BQvy7gbgynF/', 'title': 'Elephants', 'description': 'Photo by @ronan_donovan // Two bull African elephants at dawn in Uganda\'s Murchison Falls National Park. See more from Uganda with @ronan_donovan.'}, 386 | {'img': 'http://i.imgur.com/zIEjP6Q.jpg', 'link': 'https://www.instagram.com/p/BRFjVZtgSJD/', 'title': 'Westland Tai Poutini National Park', 'description': 'Photo by @christopheviseux / The Westland Tai Poutini National Park in New Zealand’s South Island offers a remarkable opportunity to take a guided walk on a glacier. A helicopter drop high on the Franz Josef Glacier, provides access to explore stunning ice formations and blue ice caves. Follow me for more images around the world @christopheviseux #newzealand #mountain #ice'}, 387 | {'img': 'http://i.imgur.com/rCrvQTv.jpg', 'link': 'https://www.instagram.com/p/BQ6_Wa2gmdR/', 'title': 'Dubai Desert Conservation Reserve', 'description': 'Photo by @christopheviseux / Early morning flight on a hot air balloon ride above the Dubai Desert Conservation Reserve. Merely an hour drive from the city, the park was created to protect indigenous species and biodiversity. The Arabian Oryx, which was close to extinction, now has a population well over 100. There are many options to explore the desert and flying above may be one of the most mesmerizing ways. Follow me @christopheviseux for more images from the Middle East. #dubai #desert'}, 388 | {'img': 'http://i.imgur.com/rCrvQTv.jpg', 'link': 'https://www.instagram.com/p/BQ6_Wa2gmdR/', 'title': 'Dubai Desert Conservation Reserve', 'description': 'Photo by @christopheviseux / Early morning flight on a hot air balloon ride above the Dubai Desert Conservation Reserve. Merely an hour drive from the city, the park was created to protect indigenous species and biodiversity. The Arabian Oryx, which was close to extinction, now has a population well over 100. There are many options to explore the desert and flying above may be one of the most mesmerizing ways. Follow me @christopheviseux for more images from the Middle East. #dubai #desert'} 389 | ] 390 | 391 | ReactExpandableGrid.defaultProps = { 392 | gridData: JSON.stringify(data), 393 | cellSize: 250, 394 | cellMargin: 25, 395 | bgColor: '#f2f2f2', 396 | detailWidth: '100%', 397 | detailHeight: 300, 398 | detailBackgroundColor: '#D9D9D9', 399 | ExpandedDetail_right_width: '60%', 400 | ExpandedDetail_left_width: '40%', 401 | ExpandedDetail_image_size: 300, 402 | ExpandedDetail_description_bgColor: '#D9D9D9', 403 | ExpandedDetail_title_bgColor: '#D9D9D9', 404 | ExpandedDetail_img_bgColor: '#D9D9D9', 405 | ExpandedDetail_link_text: '→ Link', 406 | ExpandedDetail_font_color: '#434343', 407 | ExpandedDetail_closeX_bool: true, 408 | show_mobile_style_from_width: 600, 409 | } 410 | 411 | export default ReactExpandableGrid 412 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import assert from 'assert' 3 | import TestUtils from 'react-addons-test-utils' 4 | import ReactExpandableGrid from '../src' 5 | 6 | const renderer = (Component) => { 7 | const shallowRenderer = TestUtils.createRenderer() 8 | shallowRenderer.render(Component) 9 | return shallowRenderer.getRenderOutput() 10 | } 11 | 12 | describe('ReactExpandableGrid', () => { 13 | 14 | }) 15 | --------------------------------------------------------------------------------