├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── __tests__ └── boilerplate_test.js ├── docs ├── index.js └── main.css ├── lib └── post_install.js ├── package.json ├── src ├── Camera.js └── index.js ├── style.css ├── webpack.config.babel.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react" 4 | ], 5 | "plugins": [ 6 | [ 7 | "transform-react-remove-prop-types", 8 | 9 | { 10 | "mode": "wrap" 11 | } 12 | ], 13 | ], 14 | "env": { 15 | "dev": { 16 | "presets": [ 17 | "es2015", 18 | "react-hmre" 19 | ] 20 | }, 21 | "development": { 22 | "presets": [ 23 | "es2015" 24 | ] 25 | }, 26 | "dist": { 27 | "presets": [ 28 | "es2015" 29 | ] 30 | }, 31 | "distMin": { 32 | "presets": [ 33 | "es2015" 34 | ] 35 | }, 36 | "ghPages": { 37 | "presets": [ 38 | "es2015" 39 | ] 40 | }, 41 | "modules": { 42 | "presets": [ 43 | "es2015" 44 | ] 45 | }, 46 | "es6": { 47 | "presets": [ 48 | [ 49 | "es2015", 50 | { 51 | "modules": false 52 | } 53 | ] 54 | ] 55 | }, 56 | "test": { 57 | "presets": [ 58 | "es2015" 59 | ] 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "jasmine": true, 7 | "node": true 8 | }, 9 | "plugins": [ 10 | "react" 11 | ], 12 | "rules": { 13 | "comma-dangle": ["error", "never"], 14 | "global-require": 0, 15 | "prefer-arrow-callback": 0, 16 | "func-names": 0, 17 | "import/no-extraneous-dependencies": 0, 18 | "no-underscore-dangle": 0, 19 | "no-unused-expressions": 0, 20 | "no-use-before-define": 0, 21 | "react/jsx-filename-extension": 0, 22 | "react/sort-comp": 0, 23 | "react/no-multi-comp": 0, 24 | "react/require-extension": 0 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | bin/* eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | gh-pages/ 3 | dist/ 4 | dist-es6/ 5 | dist-modules/ 6 | node_modules/ 7 | coverage/ 8 | npm-debug.log 9 | .eslintcache 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo/ 2 | dist/ 3 | tests/ 4 | src/ 5 | .* 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "6" 6 | script: 7 | - npm run test:lint 8 | - npm run test:coverage 9 | after_success: 10 | - bash <(curl -s https://codecov.io/bash) 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Jean Kévin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Camera [![Travis status build](https://travis-ci.org/Miniplop/react-camera.svg?branch=master)](https://travis-ci.org/Miniplop/react-camera/) [![npm version](https://badge.fury.io/js/react-camera.svg)](https://badge.fury.io/js/react-camera) 2 | 3 | The comprehensive camera module for React. Including photographs! (videos, and barcode scanning coming soon) 4 | 5 | ## Getting started 6 | 7 | `npm install react-camera` 8 | 9 | or 10 | 11 | `yarn add react-camera` 12 | 13 | ## Usage 14 | 15 | ``` 16 | import React, { Component } from 'react'; 17 | import Camera from 'react-camera'; 18 | 19 | export default class App extends Component { 20 | 21 | constructor(props) { 22 | super(props); 23 | this.takePicture = this.takePicture.bind(this); 24 | } 25 | 26 | takePicture() { 27 | this.camera.capture() 28 | .then(blob => { 29 | this.img.src = URL.createObjectURL(blob); 30 | this.img.onload = () => { URL.revokeObjectURL(this.src); } 31 | }) 32 | } 33 | 34 | render() { 35 | return ( 36 |
37 | { 40 | this.camera = cam; 41 | }} 42 | > 43 |
44 |
45 |
46 | 47 | { 50 | this.img = img; 51 | }} 52 | /> 53 |
54 | ); 55 | } 56 | } 57 | 58 | const style = { 59 | preview: { 60 | position: 'relative', 61 | }, 62 | captureContainer: { 63 | display: 'flex', 64 | position: 'absolute', 65 | justifyContent: 'center', 66 | zIndex: 1, 67 | bottom: 0, 68 | width: '100%' 69 | }, 70 | captureButton: { 71 | backgroundColor: '#fff', 72 | borderRadius: '50%', 73 | height: 56, 74 | width: 56, 75 | color: '#000', 76 | margin: 20 77 | }, 78 | captureImage: { 79 | width: '100%', 80 | } 81 | }; 82 | ``` 83 | 84 | ## Component instance methods 85 | 86 | You can access component methods by adding a ref (ie. ref="camera") prop to your element, then you can use this.refs.camera.capture(cb), etc. inside your component. 87 | 88 | #### `capture(): Promise` 89 | -------------------------------------------------------------------------------- /__tests__/boilerplate_test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { 3 | // renderIntoDocument, 4 | // findRenderedDOMComponentWithClass, 5 | // findRenderedDOMComponentWithTag, 6 | // Simulate 7 | // } from 'react-addons-test-utils'; 8 | import { expect } from 'chai'; 9 | 10 | describe('React Camera', function () { 11 | it('should render', function () { 12 | expect(true).to.equal(true); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require, import/no-unresolved, react/no-multi-comp */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import GithubCorner from 'react-github-corner'; 5 | import { Catalog, CodeSpecimen, ReactSpecimen } from 'catalog'; 6 | 7 | import 'purecss/build/pure.css'; 8 | import './main.css'; 9 | import '../style.css'; 10 | 11 | // Add your documentation imports here. These are available to 12 | // React specimen. Do NOT pass React here as Catalog does that. 13 | const documentationImports = {}; 14 | const title = `${NAME} v${VERSION}`; // eslint-disable-line no-undef 15 | const project = `${USER}/${NAME}`; // eslint-disable-line no-undef 16 | const pages = [ 17 | { 18 | path: '/', 19 | title: 'Introduction', 20 | component: require('../README.md') 21 | } 22 | ]; 23 | 24 | // Catalog - logoSrc="../images/logo.png" 25 | ReactDOM.render( 26 |
27 | 35 | , 40 | js: props => , 41 | jsx: props => 42 | }} 43 | title={title} 44 | /> 45 |
, 46 | document.getElementById('app') 47 | ); 48 | -------------------------------------------------------------------------------- /docs/main.css: -------------------------------------------------------------------------------- 1 | .github-corner svg { 2 | z-index: 1000; 3 | } 4 | 5 | /* TODO: insert main styles of your demo here */ 6 | -------------------------------------------------------------------------------- /lib/post_install.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // adapted based on rackt/history (MIT) 3 | // Node 0.10+ 4 | var execSync = require('child_process').execSync; 5 | var stat = require('fs').stat; 6 | 7 | // Node 0.10 check 8 | if (!execSync) { 9 | execSync = require('sync-exec'); 10 | } 11 | 12 | function exec(command) { 13 | execSync(command, { 14 | stdio: [0, 1, 2] 15 | }); 16 | } 17 | 18 | stat('dist-modules', function(error, stat) { 19 | // Skip building on Travis 20 | if (process.env.TRAVIS) { 21 | return; 22 | } 23 | 24 | if (error || !stat.isDirectory()) { 25 | exec('npm i babel-cli babel-preset-es2015 babel-preset-react'); 26 | exec('npm run dist:modules'); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-camera", 3 | "version": "0.1.2", 4 | "license": "MIT", 5 | "description": "A Camera component for React.", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/Miniplop/react-camera.git" 9 | }, 10 | "author": "Jean Kévin", 11 | "scripts": { 12 | "start": "webpack-dev-server --env dev", 13 | "test": "jest", 14 | "test:coverage": "jest --coverage", 15 | "test:watch": "jest --watch", 16 | "test:lint": "eslint . --ignore-path .gitignore --cache", 17 | "gh-pages": "webpack --env ghPages", 18 | "gh-pages:deploy": "gh-pages -d gh-pages", 19 | "gh-pages:stats": "webpack --env ghPages --profile --json > stats.json", 20 | "dist": "webpack --env dist", 21 | "dist:es6": "rimraf ./dist-es6 && BABEL_ENV=es6 babel ./src --out-dir ./dist-es6", 22 | "dist:min": "webpack --env distMin", 23 | "dist:modules": "rimraf ./dist-modules && BABEL_ENV=modules babel ./src --out-dir ./dist-modules", 24 | "preversion": "npm run test && npm run dist && npm run dist:min && git commit --allow-empty -am \"Update dist\"", 25 | "prepublish": "npm run dist:es6 && npm run dist:modules", 26 | "postpublish": "npm run gh-pages && npm run gh-pages:deploy", 27 | "postinstall": "node lib/post_install.js" 28 | }, 29 | "main": "dist-modules", 30 | "module": "dist-es6", 31 | "jsnext:main": "dist-es6", 32 | "devDependencies": { 33 | "babel-cli": "^6.24.1", 34 | "babel-core": "^6.24.1", 35 | "babel-eslint": "^7.2.3", 36 | "babel-jest": "^20.0.0", 37 | "babel-loader": "^7.0.0", 38 | "babel-plugin-transform-react-remove-prop-types": "^0.4.4", 39 | "babel-preset-es2015": "^6.24.1", 40 | "babel-preset-react": "^6.24.1", 41 | "babel-preset-react-hmre": "^1.1.1", 42 | "catalog": "^2.5.3", 43 | "chai": "^3.5.0", 44 | "clean-webpack-plugin": "^0.1.16", 45 | "css-loader": "^0.28.1", 46 | "eslint": "^3.19.0", 47 | "eslint-config-airbnb": "^14.1.0", 48 | "eslint-loader": "^1.7.1", 49 | "eslint-plugin-import": "^2.2.0", 50 | "eslint-plugin-jsx-a11y": "^4.0.0", 51 | "eslint-plugin-react": "^6.9.0", 52 | "extract-text-webpack-plugin": "^2.1.0", 53 | "file-loader": "^0.11.1", 54 | "gh-pages": "^0.12.0", 55 | "git-prepush-hook": "^1.0.2", 56 | "html-webpack-plugin": "^2.28.0", 57 | "html-webpack-template": "^6.0.1", 58 | "jest": "^20.0.0", 59 | "json-loader": "^0.5.4", 60 | "purecss": "^0.6.2", 61 | "raw-loader": "^0.5.1", 62 | "react": "^15.5.4", 63 | "react-addons-test-utils": "^15.5.1", 64 | "react-dom": "^15.5.4", 65 | "react-github-corner": "^0.3.0", 66 | "rimraf": "^2.6.1", 67 | "style-loader": "^0.17.0", 68 | "sync-exec": "^0.6.2", 69 | "system-bell-webpack-plugin": "^1.0.0", 70 | "url-loader": "^0.5.8", 71 | "webpack": "^2.5.1", 72 | "webpack-dev-server": "^2.4.5", 73 | "webpack-merge": "^4.1.0" 74 | }, 75 | "peerDependencies": { 76 | "react": ">= 0.11.2 < 16.0.0" 77 | }, 78 | "jest": { 79 | "collectCoverage": true, 80 | "moduleFileExtensions": [ 81 | "js", 82 | "jsx" 83 | ], 84 | "moduleDirectories": [ 85 | "node_modules", 86 | "packages" 87 | ] 88 | }, 89 | "keywords": [ 90 | "react", 91 | "reactjs", 92 | "camera" 93 | ], 94 | "pre-push": [ 95 | "test" 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /src/Camera.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Camera extends Component { 5 | 6 | componentWillMount() { 7 | const { video, audio } = this.props; 8 | if (navigator.mediaDevices) { 9 | navigator.mediaDevices.getUserMedia({ video, audio }) 10 | .then((mediaStream) => { 11 | this.setState({ mediaStream }); 12 | this.video.srcObject = mediaStream; 13 | this.video.play(); 14 | }) 15 | .catch(error => error); 16 | } 17 | } 18 | 19 | capture() { 20 | const mediaStreamTrack = this.state.mediaStream.getVideoTracks()[0]; 21 | const imageCapture = new window.ImageCapture(mediaStreamTrack); 22 | 23 | return imageCapture.takePhoto(); 24 | } 25 | 26 | render() { 27 | return ( 28 |
29 | { this.props.children } 30 |
32 | ); 33 | } 34 | } 35 | 36 | Camera.propTypes = { 37 | audio: PropTypes.bool, 38 | video: PropTypes.bool, 39 | children: PropTypes.element, 40 | style: PropTypes.oneOfType([ 41 | PropTypes.string, 42 | PropTypes.number 43 | ]) 44 | }; 45 | 46 | Camera.defaultProps = { 47 | audio: false, 48 | video: true, 49 | style: {}, 50 | children: null 51 | }; 52 | 53 | export default Camera; 54 | 55 | const styles = { 56 | base: { 57 | width: '100%', 58 | height: '100%' 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Camera'; 2 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* TODO: insert default styles of your component here for easy access */ 2 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import webpack from 'webpack'; 4 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 5 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 6 | import SystemBellPlugin from 'system-bell-webpack-plugin'; 7 | import CleanWebpackPlugin from 'clean-webpack-plugin'; 8 | import merge from 'webpack-merge'; 9 | 10 | const pkg = require('./package.json'); 11 | 12 | const ROOT_PATH = __dirname; 13 | const config = { 14 | paths: { 15 | dist: path.join(ROOT_PATH, 'dist'), 16 | src: path.join(ROOT_PATH, 'src'), 17 | docs: path.join(ROOT_PATH, 'docs'), 18 | ghPages: path.join(ROOT_PATH, 'gh-pages') 19 | }, 20 | filename: 'boilerplate', 21 | library: 'Boilerplate' 22 | }; 23 | 24 | const common = { 25 | resolve: { 26 | extensions: ['.js', '.css', '.png', '.jpg'] 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.js$/, 32 | enforce: 'pre', 33 | use: 'eslint-loader', 34 | include: [ 35 | config.paths.docs, 36 | config.paths.src 37 | ] 38 | }, 39 | { 40 | test: /\.md$/, 41 | use: ['catalog/lib/loader', 'raw-loader'] 42 | }, 43 | { 44 | test: /\.(jpg|png)$/, 45 | use: { 46 | loader: 'url-loader', 47 | options: { 48 | limit: 10000 49 | } 50 | } 51 | } 52 | ] 53 | }, 54 | plugins: [ 55 | new SystemBellPlugin() 56 | ] 57 | }; 58 | 59 | const siteCommon = { 60 | plugins: [ 61 | new HtmlWebpackPlugin({ 62 | template: require('html-webpack-template'), // eslint-disable-line global-require 63 | inject: false, 64 | mobile: true, 65 | title: pkg.name, 66 | appMountId: 'app' 67 | }), 68 | new webpack.DefinePlugin({ 69 | NAME: JSON.stringify(pkg.name), 70 | USER: JSON.stringify(pkg.user), 71 | VERSION: JSON.stringify(pkg.version) 72 | }) 73 | ] 74 | }; 75 | 76 | const dev = merge(common, siteCommon, { 77 | devtool: 'eval-source-map', 78 | entry: { 79 | docs: [config.paths.docs] 80 | }, 81 | plugins: [ 82 | new webpack.DefinePlugin({ 83 | 'process.env.NODE_ENV': '"development"' 84 | }), 85 | new webpack.HotModuleReplacementPlugin() 86 | ], 87 | module: { 88 | loaders: [ 89 | { 90 | test: /\.css$/, 91 | use: ['style-loader', 'css-loader'] 92 | }, 93 | { 94 | test: /\.js$/, 95 | use: { 96 | loader: 'babel-loader', 97 | options: { 98 | cacheDirectory: true 99 | } 100 | }, 101 | include: [ 102 | config.paths.docs, 103 | config.paths.src 104 | ] 105 | } 106 | ] 107 | }, 108 | devServer: { 109 | historyApiFallback: true, 110 | hot: true, 111 | inline: true, 112 | host: process.env.HOST, 113 | port: process.env.PORT, 114 | stats: 'errors-only' 115 | } 116 | }); 117 | 118 | const ghPages = merge(common, siteCommon, { 119 | entry: { 120 | app: config.paths.docs 121 | }, 122 | output: { 123 | path: config.paths.ghPages, 124 | filename: '[name].[chunkhash].js', 125 | chunkFilename: '[chunkhash].js' 126 | }, 127 | plugins: [ 128 | new CleanWebpackPlugin(['gh-pages'], { 129 | verbose: false 130 | }), 131 | new ExtractTextPlugin('[name].[chunkhash].css'), 132 | new webpack.DefinePlugin({ 133 | // This affects the react lib size 134 | 'process.env.NODE_ENV': '"production"' 135 | }), 136 | new webpack.optimize.UglifyJsPlugin({ 137 | compress: { 138 | warnings: false 139 | } 140 | }), 141 | new webpack.optimize.CommonsChunkPlugin({ 142 | name: 'vendor', 143 | minChunks: ({ resource }) => ( 144 | resource && 145 | resource.indexOf('node_modules') >= 0 && 146 | resource.match(/\.js$/) 147 | ) 148 | }) 149 | ], 150 | module: { 151 | loaders: [ 152 | { 153 | test: /\.css$/, 154 | use: ExtractTextPlugin.extract({ 155 | fallback: 'style-loader', 156 | use: 'css-loader' 157 | }) 158 | }, 159 | { 160 | test: /\.js$/, 161 | use: 'babel-loader', 162 | include: [ 163 | config.paths.docs, 164 | config.paths.src 165 | ] 166 | } 167 | ] 168 | } 169 | }); 170 | 171 | const distCommon = { 172 | devtool: 'source-map', 173 | output: { 174 | path: config.paths.dist, 175 | libraryTarget: 'umd', 176 | library: config.library 177 | }, 178 | entry: config.paths.src, 179 | externals: { 180 | react: { 181 | commonjs: 'react', 182 | commonjs2: 'react', 183 | amd: 'React', 184 | root: 'React' 185 | } 186 | }, 187 | module: { 188 | loaders: [ 189 | { 190 | test: /\.js$/, 191 | use: 'babel-loader', 192 | include: config.paths.src 193 | } 194 | ] 195 | }, 196 | plugins: [ 197 | new SystemBellPlugin() 198 | ] 199 | }; 200 | 201 | const dist = merge(distCommon, { 202 | output: { 203 | filename: `${config.filename}.js` 204 | } 205 | }); 206 | 207 | const distMin = merge(distCommon, { 208 | output: { 209 | filename: `${config.filename}.min.js` 210 | }, 211 | plugins: [ 212 | new webpack.optimize.UglifyJsPlugin({ 213 | compress: { 214 | warnings: false 215 | } 216 | }) 217 | ] 218 | }); 219 | 220 | module.exports = (env) => { 221 | process.env.BABEL_ENV = env; 222 | 223 | const targets = { 224 | dev, 225 | dist, 226 | distMin, 227 | ghPages 228 | }; 229 | 230 | return targets[env] ? targets[env] : common; 231 | }; 232 | --------------------------------------------------------------------------------