├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── app ├── .htaccess ├── .nginx.conf ├── app.js ├── components │ ├── Button │ │ ├── index.js │ │ └── styles.css │ ├── DetailView │ │ ├── index.js │ │ └── styles.css │ ├── HistoryEntry │ │ ├── index.js │ │ └── styles.css │ ├── Icon │ │ └── index.js │ ├── MapView │ │ ├── index.js │ │ └── styles.css │ ├── MatchCard │ │ ├── index.js │ │ └── styles.css │ ├── MessageBubble │ │ ├── index.js │ │ ├── styles.css │ │ └── tests │ │ │ └── index.test.js │ ├── MessengerCard │ │ ├── index.js │ │ ├── styles.css │ │ └── tests │ │ │ └── index.test.js │ ├── MessengerInput │ │ ├── index.js │ │ ├── styles.css │ │ └── tests │ │ │ └── index.test.js │ ├── Panel │ │ ├── index.js │ │ └── styles.css │ └── Text │ │ ├── index.js │ │ └── styles.css ├── containers │ ├── App │ │ ├── index.js │ │ ├── selectors.js │ │ ├── styles.css │ │ └── tests │ │ │ └── selectors.test.js │ ├── Auth │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── sagas.js │ │ └── selectors.js │ ├── Dashboard │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── sagas.js │ │ ├── selectors.js │ │ ├── styles.css │ │ └── tests │ │ │ ├── actions.test.js │ │ │ ├── index.test.js │ │ │ ├── reducer.test.js │ │ │ ├── sagas.test.js │ │ │ └── selectors.test.js │ ├── HomePage │ │ ├── index.js │ │ └── messages.js │ ├── LanguageProvider │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── selectors.js │ ├── MainDashboard │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── sagas.js │ │ ├── selectors.js │ │ └── styles.css │ ├── Messages │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── sagas.js │ │ ├── selectors.js │ │ ├── styles.css │ │ └── tests │ │ │ ├── actions.test.js │ │ │ ├── index.test.js │ │ │ ├── reducer.test.js │ │ │ ├── sagas.test.js │ │ │ └── selectors.test.js │ ├── Navigation │ │ ├── index.js │ │ └── styles.css │ ├── NotFoundPage │ │ ├── index.js │ │ └── messages.js │ ├── Notification │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── sagas.js │ │ ├── selectors.js │ │ └── styles.css │ └── Recommendations │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── sagas.js │ │ ├── selectors.js │ │ └── styles.css ├── favicon.ico ├── global_constants.js ├── i18n.js ├── index.html ├── manifest.json ├── reducers.js ├── routes.js ├── static │ ├── conversation.png │ ├── tinder_female.png │ └── tinder_male.png ├── store.js ├── store.test.js ├── tests │ └── store.test.js ├── translations │ ├── de.json │ └── en.json └── utils │ ├── asyncInjectors.js │ ├── facebook.js │ ├── history.js │ ├── logger.js │ ├── notifications.js │ ├── operations.js │ ├── request.js │ ├── storage.js │ └── tests │ └── asyncInjectors.test.js ├── appveyor.yml ├── chrome ├── chrome.crx └── src │ ├── ext │ ├── FBT.js │ ├── Facebook.js │ ├── Lit.js │ └── background.js │ ├── icons │ ├── 128x128.png │ ├── 16x16.png │ ├── 48x48.png │ └── main.png │ ├── manifest.json │ └── vendor │ └── jquery.min.js ├── internals ├── config.js ├── generators │ ├── component │ │ ├── es6.js.hbs │ │ ├── index.js │ │ ├── messages.js.hbs │ │ ├── stateless.js.hbs │ │ ├── styles.css.hbs │ │ └── test.js.hbs │ ├── container │ │ ├── actions.js.hbs │ │ ├── actions.test.js.hbs │ │ ├── constants.js.hbs │ │ ├── index.js │ │ ├── index.js.hbs │ │ ├── messages.js.hbs │ │ ├── reducer.js.hbs │ │ ├── reducer.test.js.hbs │ │ ├── sagas.js.hbs │ │ ├── sagas.test.js.hbs │ │ ├── selectors.js.hbs │ │ ├── selectors.test.js.hbs │ │ ├── styles.css.hbs │ │ └── test.js.hbs │ ├── index.js │ ├── language │ │ ├── add-locale-data.hbs │ │ ├── app-locale.hbs │ │ ├── format-translation-messages.hbs │ │ ├── index.js │ │ ├── intl-locale-data.hbs │ │ ├── polyfill-intl-locale.hbs │ │ ├── translation-messages.hbs │ │ └── translations-json.hbs │ ├── route │ │ ├── index.js │ │ ├── route.hbs │ │ └── routeWithReducer.hbs │ └── utils │ │ └── componentExists.js ├── scripts │ ├── analyze.js │ ├── clean.js │ ├── dependencies.js │ ├── extract-intl.js │ ├── helpers │ │ ├── checkmark.js │ │ └── progress.js │ ├── npmcheckversion.js │ └── pagespeed.js ├── testing │ ├── karma.conf.js │ └── test-bundler.js └── webpack │ ├── webpack.base.babel.js │ ├── webpack.dev.babel.js │ ├── webpack.dll.babel.js │ ├── webpack.prod.babel.js │ └── webpack.test.babel.js ├── media ├── messages.png ├── profile.png └── recommendations.png ├── package.json ├── server ├── api │ ├── api.js │ ├── facebookAuth.js │ ├── promiseTinder.js │ └── shelvedApi.txt ├── index.js ├── logger.js └── middlewares │ └── frontendMiddleware.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # 12 | ## These files are text and should be normalized (Convert crlf => lf) 13 | # 14 | 15 | # source code 16 | *.php text 17 | *.css text 18 | *.sass text 19 | *.scss text 20 | *.less text 21 | *.styl text 22 | *.js text eol=lf 23 | *.coffee text 24 | *.json text 25 | *.htm text 26 | *.html text 27 | *.xml text 28 | *.svg text 29 | *.txt text 30 | *.ini text 31 | *.inc text 32 | *.pl text 33 | *.rb text 34 | *.py text 35 | *.scm text 36 | *.sql text 37 | *.sh text 38 | *.bat text 39 | 40 | # templates 41 | *.ejs text 42 | *.hbt text 43 | *.jade text 44 | *.haml text 45 | *.hbs text 46 | *.dot text 47 | *.tmpl text 48 | *.phtml text 49 | 50 | # server config 51 | .htaccess text 52 | 53 | # git config 54 | .gitattributes text 55 | .gitignore text 56 | .gitconfig text 57 | 58 | # code analysis config 59 | .jshintrc text 60 | .jscsrc text 61 | .jshintignore text 62 | .csslintrc text 63 | 64 | # misc config 65 | *.yaml text 66 | *.yml text 67 | .editorconfig text 68 | 69 | # build config 70 | *.npmignore text 71 | *.bowerrc text 72 | 73 | # Heroku 74 | Procfile text 75 | .slugignore text 76 | 77 | # Documentation 78 | *.md text 79 | LICENSE text 80 | AUTHORS text 81 | 82 | 83 | # 84 | ## These files are binary and should be left untouched 85 | # 86 | 87 | # (binary is a macro for -text -diff) 88 | *.png binary 89 | *.jpg binary 90 | *.jpeg binary 91 | *.gif binary 92 | *.ico binary 93 | *.mov binary 94 | *.mp4 binary 95 | *.mp3 binary 96 | *.flv binary 97 | *.fla binary 98 | *.swf binary 99 | *.gz binary 100 | *.zip binary 101 | *.7z binary 102 | *.ttf binary 103 | *.eot binary 104 | *.woff binary 105 | *.pyc binary 106 | *.pdf binary 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | build 4 | node_modules 5 | stats.json 6 | 7 | # Cruft 8 | .DS_Store 9 | npm-debug.log 10 | .idea 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: true 3 | dist: trusty 4 | node_js: 5 | - "5.0" 6 | script: npm run build 7 | before_install: 8 | - export CHROME_BIN=/usr/bin/google-chrome 9 | - export DISPLAY=:99.0 10 | - sudo apt-get update 11 | - sudo apt-get install -y libappindicator1 fonts-liberation 12 | - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 13 | - sudo dpkg -i google-chrome*.deb 14 | - sh -e /etc/init.d/xvfb start 15 | notifications: 16 | email: 17 | on_failure: change 18 | after_success: 'npm run coveralls' 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Maximilian Stoiber 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Tinder 2 | Super Charged Tinder web client - Incremental Storage, recommendation filtering, live chat, web notifications 3 | **Currently only supports Chrome due to Chrome Extension support** - Read more at https://medium.com/@paulxuca/tinder-tales-or-the-search-for-tinders-new-api-4d3a36e2542#.1ab1cvutd 4 | 5 | # Introduction 6 | A tinder web client built in React & Redux Saga - 7 | React Tinder offers all the same features as the Tinder mobile application with some bonuses: 8 | - Web notifications! Get notified when you get a new match or message. 9 | - Incremental Storage: Have thousands of matches? Don't download them all again. Only load the **new** matches and messages. 10 | - Figure out who has already liked you! So you only like those people and not waste your likes. (This feature only works if **tinder** can provide us with data to indicate as such thanks to @GrandmasterMeio) 11 | - Want to pass instead of like? Go back to your history and change your mind in history. 12 | - Live chat with matches. 13 | - Update your location to travel around the world. 14 | 15 | # Images 16 | ![Profile Screen](https://raw.githubusercontent.com/litdevelopers/tinder/master/media/profile.png) 17 | ![Recommendations Screen](https://raw.githubusercontent.com/litdevelopers/tinder/master/media/recommendations.png) 18 | ![Messaging Screen](https://raw.githubusercontent.com/litdevelopers/tinder/master/media/messages.png) 19 | 20 | # Usage 21 | 1. Fork or Clone the repository. 22 | 2. run ```npm install``` while in the directory of the project. 23 | 3. Open chrome and load in a new "unpacked extension" from the extensions screen. 24 | 4. Open project folder and load in the ```src``` folder in ```/chrome```. 25 | 5. Open ```/app/containers/auth/sagas.js/``` and edit the variable CHROME_EXTENSION_ID to the one found in your chrome extensions screen. 26 | 6. run ```npm start``` in the root directory of the project. 27 | 7. Navigate to ```localhost:3000/login``` and the chrome extension will handle the login process. 28 | 8. Find your soulmate. 29 | 30 | # Road Map 31 | This project is still a heavy WIP (Work in Progress) and pull requests are always welcome. Todo: 32 | 1. Polish up UI of the application 33 | 2. Figure out a better way of handling high volume accounts (1000+ matches) 34 | 3. Code refactoring and linting 35 | 4. Implement matches searching and indexing 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ####################################################################### 5 | # GENERAL # 6 | ####################################################################### 7 | 8 | # Make apache follow sym links to files 9 | Options +FollowSymLinks 10 | # If somebody opens a folder, hide all files from the resulting folder list 11 | IndexIgnore */* 12 | 13 | 14 | ####################################################################### 15 | # REWRITING # 16 | ####################################################################### 17 | 18 | # Enable rewriting 19 | RewriteEngine On 20 | 21 | # If its not HTTPS 22 | RewriteCond %{HTTPS} off 23 | 24 | # Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL 25 | # RewriteCond %{HTTP:X-Forwarded-Proto} !https 26 | 27 | # Redirect to the same URL with https://, ignoring all further rules if this one is in effect 28 | RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L] 29 | 30 | # If we get to here, it means we are on https:// 31 | 32 | # If the file with the specified name in the browser doesn't exist 33 | RewriteCond %{REQUEST_FILENAME} !-f 34 | 35 | # and the directory with the specified name in the browser doesn't exist 36 | RewriteCond %{REQUEST_FILENAME} !-d 37 | 38 | # and we are not opening the root already (otherwise we get a redirect loop) 39 | RewriteCond %{REQUEST_FILENAME} !\/$ 40 | 41 | # Rewrite all requests to the root 42 | RewriteRule ^(.*) / 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/.nginx.conf: -------------------------------------------------------------------------------- 1 | ## 2 | # Put this file in /etc/nginx/conf.d folder and make sure 3 | # you have line 'include /etc/nginx/conf.d/*.conf;' 4 | # in your main nginx configuration file 5 | ## 6 | 7 | ## 8 | # Redirect to the same URL with https:// 9 | ## 10 | 11 | server { 12 | 13 | listen 80; 14 | 15 | # Type your domain name below 16 | server_name example.com; 17 | 18 | return 301 https://$server_name$request_uri; 19 | 20 | } 21 | 22 | ## 23 | # HTTPS configurations 24 | ## 25 | 26 | server { 27 | 28 | listen 443; 29 | 30 | # Type your domain name below 31 | server_name example.com; 32 | 33 | ssl on; 34 | ssl_certificate /path/to/certificate.crt; 35 | ssl_certificate_key /path/to/server.key; 36 | 37 | # Use only TSL protocols for more secure 38 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 39 | 40 | # Always serve index.html for any request 41 | location / { 42 | # Set path 43 | root /var/www/; 44 | try_files $uri /index.html; 45 | } 46 | 47 | ## 48 | # If you want to use Node/Rails/etc. API server 49 | # on the same port (443) config Nginx as a reverse proxy. 50 | # For security reasons use a firewall like ufw in Ubuntu 51 | # and deny port 3000/tcp. 52 | ## 53 | 54 | # location /api/ { 55 | # 56 | # proxy_pass http://localhost:3000; 57 | # proxy_http_version 1.1; 58 | # proxy_set_header X-Forwarded-Proto https; 59 | # proxy_set_header Upgrade $http_upgrade; 60 | # proxy_set_header Connection 'upgrade'; 61 | # proxy_set_header Host $host; 62 | # proxy_cache_bypass $http_upgrade; 63 | # 64 | # } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * This is the entry file for the application, only setup and boilerplate 5 | * code. 6 | */ 7 | import 'babel-polyfill'; 8 | 9 | /* eslint-disable import/no-unresolved */ 10 | // Load the manifest.json file and the .htaccess file 11 | import 'file?name=[name].[ext]!./favicon.ico'; 12 | import '!file?name=[name].[ext]!./manifest.json'; 13 | import 'file?name=[name].[ext]!./.htaccess'; 14 | 15 | /* eslint-enable import/no-unresolved */ 16 | 17 | // Import all the third party stuff 18 | import React from 'react'; 19 | import ReactDOM from 'react-dom'; 20 | import { Provider } from 'react-redux'; 21 | import { applyRouterMiddleware, Router, browserHistory } from 'react-router'; 22 | import { syncHistoryWithStore } from 'react-router-redux'; 23 | import useScroll from 'react-router-scroll'; 24 | import LanguageProvider from 'containers/LanguageProvider'; 25 | import configureStore from './store'; 26 | 27 | // Import i18n messages 28 | import { translationMessages } from './i18n'; 29 | 30 | // Import the CSS reset, which HtmlWebpackPlugin transfers to the build folder 31 | import 'sanitize.css/sanitize.css'; 32 | import 'react-image-gallery/build/image-gallery.css'; 33 | import 'rheostat/css/slider.css'; 34 | import 'rheostat/css/slider-vertical.css'; 35 | import 'rheostat/css/slider-horizontal.css'; 36 | 37 | // Create redux store with history 38 | // this uses the singleton browserHistory provided by react-router 39 | // Optionally, this could be changed to leverage a created history 40 | // e.g. `const browserHistory = useRouterHistory(createBrowserHistory)();` 41 | const initialState = {}; 42 | const store = configureStore(initialState, browserHistory); 43 | 44 | // Sync history and store, as the react-router-redux reducer 45 | // is under the non-default key ("routing"), selectLocationState 46 | // must be provided for resolving how to retrieve the "route" in the state 47 | import { selectLocationState } from 'containers/App/selectors'; 48 | const history = syncHistoryWithStore(browserHistory, store, { 49 | selectLocationState: selectLocationState(), 50 | }); 51 | 52 | // Set up the router, wrapping all Routes in the App component 53 | import App from 'containers/App'; 54 | import createRoutes from './routes'; 55 | const rootRoute = { 56 | component: App, 57 | childRoutes: createRoutes(store), 58 | }; 59 | 60 | 61 | const render = (translatedMessages) => { 62 | ReactDOM.render( 63 | 64 | 65 | 74 | 75 | , 76 | document.getElementById('app') 77 | ); 78 | }; 79 | 80 | 81 | // Hot reloadable translation json files 82 | if (module.hot) { 83 | // modules.hot.accept does not accept dynamic dependencies, 84 | // have to be constants at compile-time 85 | module.hot.accept('./i18n', () => { 86 | render(translationMessages); 87 | }); 88 | } 89 | 90 | // Chunked polyfill for browsers without Intl support 91 | if (!window.Intl) { 92 | Promise.all([ 93 | System.import('intl'), 94 | System.import('intl/locale-data/jsonp/en.js'), 95 | ]).then(() => render(translationMessages)); 96 | } else { 97 | render(translationMessages); 98 | } 99 | 100 | // Install ServiceWorker and AppCache in the end since 101 | // it's not most important operation and if main code fails, 102 | // we do not want it installed 103 | import { install } from 'offline-plugin/runtime'; 104 | install(); 105 | -------------------------------------------------------------------------------- /app/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import styles from './styles.css'; 3 | import Icon from 'components/Icon'; 4 | 5 | 6 | const styleMapping = { 7 | superlike: { 8 | marginBottom: 3, 9 | }, 10 | pass: { 11 | left: 4, 12 | marginLeft: 3, 13 | }, 14 | }; 15 | 16 | const Button = ({ children, type, id, hash, details, onClick, ...props }) => ( 17 | ); 26 | 27 | Button.propTypes = { 28 | children: PropTypes.oneOfType([ 29 | PropTypes.string, 30 | PropTypes.node, 31 | ]), 32 | type: PropTypes.string, 33 | id: PropTypes.string, 34 | hash: PropTypes.string, 35 | onClick: PropTypes.func.isRequired, 36 | details: PropTypes.object, 37 | }; 38 | 39 | export default Button; 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/components/Button/styles.css: -------------------------------------------------------------------------------- 1 | .button { 2 | padding: 5px 5px; 3 | border: 2px solid; 4 | color: white; 5 | font-size: 10px; 6 | margin:5px; 7 | } 8 | 9 | .locationToggle { 10 | composes: button; 11 | position: absolute; 12 | z-index: 3; 13 | right: 0; 14 | border-color: #c4c4c4; 15 | color: #484848; 16 | } 17 | 18 | .locationToggle:focus { 19 | outline: 0; 20 | } 21 | 22 | .commonInterest { 23 | composes: button; 24 | font-size: 12px; 25 | font-weight: 500; 26 | padding: 5px 15px; 27 | border: 2px solid #DB4C2C; 28 | color: #DB4C2C; 29 | border-radius: 15px; 30 | margin-right: 5px; 31 | } 32 | 33 | .buttonMatch { 34 | composes: button; 35 | cursor: pointer; 36 | height: 50px; 37 | width: 50px; 38 | font-weight: 600; 39 | border-radius: 50px; 40 | } 41 | 42 | .buttonMatch:focus { 43 | outline: 0; 44 | } 45 | 46 | .pass { 47 | composes: buttonMatch; 48 | color: #e95f5c; 49 | border-color: #e95f5c; 50 | } 51 | 52 | .pass:hover{ 53 | background-color: #e95f5c; 54 | color: white; 55 | transition: 0.25s; 56 | } 57 | 58 | .superlike { 59 | composes: buttonMatch; 60 | color: #5cc3e8; 61 | border-color: #5cc3e8; 62 | } 63 | 64 | .superlike:hover{ 65 | color: white; 66 | background-color: #5cc3e8; 67 | transition: 0.25s; 68 | } 69 | 70 | .like { 71 | composes: buttonMatch; 72 | color: #79ceb8; 73 | border-color: #79ceb8; 74 | } 75 | 76 | .like:hover{ 77 | background-color: #79ceb8; 78 | color: white; 79 | transition: 0.25s; 80 | } 81 | 82 | .fetchMatches, .unmatch { 83 | cursor: pointer; 84 | font-size: 12px; 85 | max-height: 30px; 86 | padding: 5px 15px; 87 | border-radius: 3px; 88 | margin-left: 10px; 89 | color: white; 90 | font-weight: 500; 91 | background-color: #e95f5c; 92 | border: 1px solid #e95f5c; 93 | } 94 | 95 | .fetchMatchesHollow { 96 | composes: fetchMatches; 97 | background-color: transparent; 98 | color: #e95f5c; 99 | } 100 | 101 | .fetchMatches:hover, .unmatch:hover { 102 | background-color: transparent; 103 | color: #e95f5c; 104 | transition: 0.25s; 105 | } 106 | 107 | .fetchMatchesHollow:hover { 108 | background-color: #e95f5c; 109 | color: white; 110 | transition: 0.25s; 111 | } 112 | 113 | .accountSettings { 114 | flex: 1; 115 | background: white; 116 | border: 1px solid #eee; 117 | padding: 10px 20px; 118 | margin: 10px; 119 | } -------------------------------------------------------------------------------- /app/components/DetailView/styles.css: -------------------------------------------------------------------------------- 1 | .detailViewContainer { 2 | display: flex; 3 | flex-basis: 100%; 4 | flex-direction: column; 5 | flex:1; 6 | } 7 | 8 | .detailViewContainer_mainPicture { 9 | overflow: hidden; 10 | } 11 | 12 | .detailViewContainer_content { 13 | flex: 3; 14 | padding: 20px 10px; 15 | overflow-y: scroll; 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | .commonConnectionsContainer { 21 | margin: 10px 0px; 22 | flex-direction: row; 23 | display: flex; 24 | overflow-x: scroll; 25 | } 26 | 27 | .connectionItem { 28 | display: flex; 29 | width: 50px; 30 | height: 100%; 31 | margin:5px; 32 | align-items: center; 33 | flex-direction: column; 34 | } 35 | 36 | .connectionImage { 37 | flex: 1; 38 | height: 50px; 39 | width: 50px; 40 | background-position: center; 41 | background-size: cover; 42 | border-radius: 50px; 43 | } 44 | 45 | .commonInterestsWrapper { 46 | white-space: nowrap; 47 | } 48 | 49 | .commonInterestsContainer { 50 | margin: 10px 0px; 51 | flex-direction: row; 52 | display: flex; 53 | overflow-x: scroll; 54 | } 55 | 56 | .commonInterestsLink { 57 | padding: 10px 5px; 58 | text-decoration: none; 59 | } 60 | 61 | .detailViewContainerButtons { 62 | justify-content: space-around; 63 | display: flex; 64 | width: 60%; 65 | margin: 80px auto 0px auto; 66 | flex: 1; 67 | align-self: flex-end; 68 | align-items: flex-end; 69 | } 70 | 71 | .detailViewContainerButtons button { 72 | flex: 1; 73 | max-width: 50px; 74 | max-height: 50px; 75 | } 76 | 77 | -------------------------------------------------------------------------------- /app/components/HistoryEntry/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from 'components/Button'; 3 | import Text from 'components/Text'; 4 | import styles from './styles.css'; 5 | 6 | import { parsePingTime } from 'utils/operations'; 7 | 8 | const buttonTypes = [ 9 | { 10 | type: 'LIKE_PERSON', 11 | buttonText: 'like', 12 | }, 13 | { 14 | type: 'PASS_PERSON', 15 | buttonText: 'pass', 16 | }, 17 | { 18 | type: 'SUPERLIKE_PERSON', 19 | buttonText: 'superlike', 20 | }, 21 | ]; 22 | 23 | 24 | const typeMapping = { 25 | LIKE_PERSON: (personName) => You liked {personName}, 26 | SUPERLIKE_PERSON: (personName) => You superliked {personName}, 27 | PASS_PERSON: (personName) => You passed on {personName}, 28 | NEW_MATCH: (personName) => You matched with {personName}, 29 | }; 30 | 31 | function renderText(type, details) { 32 | return typeMapping[type](details.name); 33 | } 34 | 35 | function renderDate({ date }) { 36 | return parsePingTime(date, false); 37 | } 38 | 39 | const HistoryEntry = ({ data, onClickButton }) => { 40 | return ( 41 |
  • 42 |
    43 |
    44 | {renderText(data.type, data.details)} 45 | {renderDate(data.details)} 46 |
    47 |
    48 | {data.type !== 'NEW_MATCH' && buttonTypes.map((each) => { 49 | if (each.type !== data.type) { 50 | return ( 51 | ); 65 | } 66 | })} 67 |
    68 |
    69 |
  • 70 | ); 71 | }; 72 | 73 | HistoryEntry.propTypes = { 74 | data: React.PropTypes.object, 75 | onClickButton: React.PropTypes.func, 76 | }; 77 | 78 | 79 | export default HistoryEntry; 80 | -------------------------------------------------------------------------------- /app/components/HistoryEntry/styles.css: -------------------------------------------------------------------------------- 1 | .historyEntryContainer { 2 | list-style: none; 3 | flex: 1; 4 | width: 100%; 5 | border-bottom: 1px solid #EEE; 6 | margin: 0; 7 | display: flex; 8 | flex-direction: row; 9 | } 10 | 11 | .historyEntryContainerContent { 12 | padding: 10px; 13 | display: flex; 14 | flex: 1; 15 | } 16 | 17 | .textContainer { 18 | flex: 2; 19 | display: flex; 20 | align-content: center; 21 | align-items: flex-end; 22 | } 23 | 24 | .buttonsContainer { 25 | flex: 1; 26 | align-items: center; 27 | font-size: 12px; 28 | justify-content: flex-end; 29 | display: flex; 30 | } 31 | -------------------------------------------------------------------------------- /app/components/MapView/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { default as ScriptjsLoader } from 'react-google-maps/lib/async/ScriptjsLoader'; 3 | import GoogleMap from 'react-google-maps/lib/GoogleMap'; 4 | import Marker from 'react-google-maps/lib/Marker'; 5 | import { triggerEvent } from 'react-google-maps/lib/utils'; 6 | import Button from 'components/Button'; 7 | import styles from './styles.css'; 8 | 9 | export default class MapView extends React.Component { 10 | constructor() { 11 | super(); 12 | this.handleResize = this.handleResize.bind(this); 13 | this.handleButtonClick = this.handleButtonClick.bind(this); 14 | this.listener = null; 15 | } 16 | 17 | componentWillUnmount() { 18 | google.maps.event.removeListener(this.listener); // eslint-disable-line 19 | } 20 | 21 | handleButtonClick() { 22 | this.props.onClick(); 23 | this.listener = google.maps.event.addListener(this.googleMapComponent.props.map, 'idle', () => { // eslint-disable-line 24 | google.maps.event.trigger(this.googleMapComponent.props.map, 'resize'); // eslint-disable-line 25 | }); 26 | } 27 | 28 | handleResize() { 29 | triggerEvent(this.googleMapComponent, 'resize'); 30 | } 31 | 32 | 33 | render() { 34 | return ( 35 |
    36 | 43 |
    } 48 | containerElement={ 49 |
    50 | } 51 | googleMapElement={ 52 | { 54 | if (!googleMap) { 55 | return; 56 | } 57 | this.googleMapComponent = googleMap; 58 | }} 59 | defaultOptions={{ mapTypeControl: false, streetViewControl: false }} 60 | defaultZoom={6} 61 | defaultCenter={this.props.markerLocation.lat ? this.props.markerLocation : { lat: 43.6532, lng: -79.3832 }} 62 | onClick={(data) => { 63 | if (this.props.open) this.props.onSelectMarker(data.latLng.lat(), data.latLng.lng()); 64 | }} 65 | > 66 | {this.props.markerLocation.lat && this.props.markerLocation.lng ? : null} 67 | 68 | } 69 | /> 70 |
    71 | ); 72 | } 73 | } 74 | 75 | MapView.propTypes = { 76 | onSelectMarker: React.PropTypes.func, 77 | onClick: React.PropTypes.func, 78 | markerLocation: React.PropTypes.object, 79 | open: React.PropTypes.bool.isRequired, 80 | }; 81 | -------------------------------------------------------------------------------- /app/components/MapView/styles.css: -------------------------------------------------------------------------------- 1 | .mapViewContainer { 2 | height: 100%; 3 | width: 100%; 4 | position: relative; 5 | } -------------------------------------------------------------------------------- /app/components/MatchCard/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { getAge, convertDistanceToLocal } from 'utils/operations'; 3 | 4 | import styles from './styles.css'; 5 | import Text from 'components/Text'; 6 | import Button from 'components/Button'; 7 | 8 | const buttonMapping = [ 9 | 'pass', 'superlike', 'like', 10 | ]; 11 | 12 | 13 | const MatchCard = ({ data, type, onClick, onClickButton }) => { 14 | const bioText = (data.bio && data.bio.trim()) ? 15 |

    {data.bio}

    : 16 | null; 17 | const ageText = ( 18 | {getAge(data.birth_date)} 19 | 20 | {convertDistanceToLocal(data.distance_mi)} km away 21 | 22 | ); 23 | const schoolText = (data.schools[0] && data.schools[0].name) ? {data.schools[0].name} : null; 24 | 25 | const detailDiv = (schoolText || bioText) ? 26 |
    27 | {/* {bioText} */} 28 |
    29 | {schoolText} 30 |
    31 |
    : null; 32 | 33 | /* eslint-disable no-underscore-dangle */ 34 | return ( 35 |
    42 |
    43 |
    44 | {buttonMapping.map((each) => )} 45 |
    46 |
    { 49 | onClick(data._id, data.photos[0].url); 50 | }} 51 | > 52 | {ageText} 53 | 54 | {data.name} 55 | 56 | {detailDiv} 57 |
    58 |
    59 |
    60 | ); 61 | /* eslint-enable no-underscore-dangle */ 62 | }; 63 | 64 | MatchCard.propTypes = { 65 | onClickButton: PropTypes.func.isRequired, 66 | onClick: PropTypes.func.isRequired, 67 | data: PropTypes.object.isRequired, 68 | tab_index: PropTypes.number, 69 | type: PropTypes.string, 70 | }; 71 | 72 | 73 | export default MatchCard; 74 | 75 | -------------------------------------------------------------------------------- /app/components/MatchCard/styles.css: -------------------------------------------------------------------------------- 1 | .matchCard{ 2 | display: flex !important; 3 | border-radius: 5px; 4 | min-height: 350px; 5 | width: 30%; 6 | max-width: 300px; 7 | margin:10px; 8 | background-size: cover; 9 | background-position: center; 10 | background-repeat: no-repeat; 11 | box-shadow: 0px 4px 6px 0px rgba(0,0,0,0.22); 12 | cursor: pointer; 13 | } 14 | 15 | .matchCardLike { 16 | border: 2px solid #FF6966; 17 | } 18 | 19 | @keyframes drop { 20 | 0% { 21 | transform: scale(1); 22 | box-shadow: 0px 4px 6px 0px rgba(0,0,0,0.22); 23 | } 24 | 50% { 25 | transform: scale(1.005); 26 | box-shadow: 0px 14px 20px 0px rgba(0,0,0,0.32); 27 | } 28 | 100% { 29 | transform: scale(1); 30 | box-shadow: 0px 4px 6px 0px rgba(0,0,0,0.22); 31 | } 32 | } 33 | 34 | .matchCard:hover { 35 | animation: drop 1.5s infinite ease-in-out; 36 | } 37 | 38 | .matchCard:hover .matchCardContainer_hide{ 39 | height: initial; 40 | visibility: visible; 41 | transition: 0.3s; 42 | } 43 | 44 | .matchCard:hover .matchCardButtons { 45 | visibility: visible; 46 | transition: 0.3s; 47 | } 48 | 49 | .matchCardContainer { 50 | display:flex; 51 | height:100%; 52 | width:100%; 53 | border-radius: 3px; 54 | background-color: rgba(0,0,0,0.4); 55 | padding:15px; 56 | justify-content: flex-end; 57 | flex-direction: column; 58 | } 59 | 60 | 61 | .matchCardButtons { 62 | flex:1; 63 | flex-direction: column; 64 | align-content: flex-start; 65 | visibility: hidden; 66 | display: flex; 67 | flex-direction: row; 68 | justify-content: space-around; 69 | } 70 | 71 | .matchCardButtons > button > object { 72 | position: relative; 73 | height: 50%; 74 | width: 50%; 75 | pointer-events: none; 76 | } 77 | 78 | .matchCardContainer_bio { 79 | flex-basis: 10%; 80 | color: #FFF; 81 | font-weight:300; 82 | font-size:12px; 83 | overflow: hidden; 84 | max-height: 160px; 85 | text-overflow: ellipsis; 86 | } 87 | 88 | .matchCardContainer_details { 89 | flex-basis: 5%; 90 | display: flex; 91 | flex-direction: column; 92 | margin-bottom:10px; 93 | 94 | } 95 | 96 | 97 | .matchCardContainer_hide { 98 | height: 0%; 99 | visibility: hidden; 100 | } 101 | 102 | .matchCardContainer_wrapper { 103 | flex: 7; 104 | display: flex; 105 | flex-direction: column; 106 | justify-content: flex-end; 107 | } -------------------------------------------------------------------------------- /app/components/MessageBubble/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * MessageBubble 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import styles from './styles.css'; 9 | import { parsePingTime } from 'utils/operations'; 10 | 11 | const MessageBubble = ({ from, children, date }) => ( 12 |
    13 | {children.match(/media0|giphy/) ? : 14 | {children} 15 | } 16 | {parsePingTime(date, false)} 17 |
    18 | ); 19 | 20 | MessageBubble.propTypes = { 21 | from: PropTypes.string.isRequired, 22 | children: PropTypes.string, 23 | date: PropTypes.string, 24 | }; 25 | 26 | export default MessageBubble; 27 | -------------------------------------------------------------------------------- /app/components/MessageBubble/styles.css: -------------------------------------------------------------------------------- 1 | .messageBubble { 2 | font-size: 16px; 3 | position: relative; 4 | display: inline-block; 5 | clear: both; 6 | margin-bottom: 8px; 7 | padding: 13px 14px; 8 | vertical-align: top; 9 | border-radius: 5px; 10 | -webkit-box-sizing: content-box !important; 11 | -moz-box-sizing: content-box !important; 12 | box-sizing: content-box !important; 13 | max-width: 80%; 14 | } 15 | 16 | .me { 17 | color: #FFF; 18 | background-color: #1aafd0; 19 | composes: messageBubble; 20 | float: left; 21 | align-self: flex-start; 22 | } 23 | 24 | .you { 25 | align-self: flex-end; 26 | color: #1a1a1a; 27 | background-color: #eceff1; 28 | composes: messageBubble; 29 | } 30 | 31 | .you:before { 32 | right: -3px; 33 | background-color: #eceff1; 34 | position: absolute; 35 | top: 19px; 36 | display: block; 37 | width: 8px; 38 | height: 6px; 39 | content: '\00a0'; 40 | -webkit-transform: rotate(29deg) skew(-35deg); 41 | transform: rotate(29deg) skew(-35deg); 42 | } 43 | 44 | .messageDate { 45 | color: #aaa; 46 | font-size: 12px; 47 | left: 0; 48 | visibility: hidden; 49 | display: none; 50 | margin-left: 3px; 51 | white-space: nowrap; 52 | } 53 | 54 | .you .messageDate { 55 | color: black; 56 | } 57 | 58 | .you:active .messageDate{ 59 | visibility: visible; 60 | display: inline; 61 | } 62 | 63 | .me:active .messageDate{ 64 | visibility: visible; 65 | display: inline; 66 | color: white; 67 | } 68 | 69 | .me:before { 70 | left: -3px; 71 | background-color: #1aafd0; 72 | position: absolute; 73 | top: 19px; 74 | display: block; 75 | width: 8px; 76 | height: 6px; 77 | content: '\00a0'; 78 | -webkit-transform: rotate(29deg) skew(-35deg); 79 | transform: rotate(29deg) skew(-35deg); 80 | } -------------------------------------------------------------------------------- /app/components/MessageBubble/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import MessageBubble from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/MessengerCard/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * MatchMessengerBlock 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import { parsePingTime } from 'utils/operations'; 9 | 10 | import Text from 'components/Text'; 11 | import styles from './styles.css'; 12 | import Icon from 'components/Icon'; 13 | 14 | const MessengerCard = ({ data, onClick, isNew, isReply }) => { 15 | const person = data.person; 16 | const recentActivity = parsePingTime(data.last_activity_date, false); 17 | const messages = data.messages; 18 | const recentMessage = messages && messages[messages.length - 1] && messages[messages.length - 1].message; 19 | if (person) { 20 | return ( 21 |
    onClick(person._id)} className={styles.matchBlock}> 22 |
    23 |
    30 |
    31 |
    32 |
    33 | {person && person.name} {isNew ?
    : null} 34 | {recentActivity} 35 |
    36 | {recentMessage ? 39 | {isReply ? : null} 44 | {recentMessage.slice(0, 50)} 45 | : null} 46 |
    47 |
    48 | ); 49 | } 50 | return null; 51 | }; 52 | 53 | MessengerCard.propTypes = { 54 | data: PropTypes.object.isRequired, 55 | onClick: PropTypes.func.isRequired, 56 | isNew: PropTypes.bool, 57 | isReply: PropTypes.bool, 58 | }; 59 | 60 | export default MessengerCard; 61 | -------------------------------------------------------------------------------- /app/components/MessengerCard/styles.css: -------------------------------------------------------------------------------- 1 | .matchBlock { 2 | display: flex; 3 | flex: 1; 4 | border-bottom: 1px solid #eee; 5 | cursor: pointer; 6 | } 7 | 8 | .matchAvatarContainer { 9 | 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | flex: 1; 14 | height: 100px; 15 | } 16 | 17 | .matchAvatar { 18 | height: 60px; 19 | max-width: 60px; 20 | border-radius: 60px; 21 | flex: 1; 22 | background-position: center; 23 | background-size: cover; 24 | background-repeat: no-repeat; 25 | } 26 | 27 | .matchDetails { 28 | flex: 2; 29 | display: inline-block; 30 | overflow:hidden; 31 | padding: 20px; 32 | text-overflow: ellipsis; 33 | } 34 | 35 | .flexName { 36 | display: flex; 37 | align-items: baseline; 38 | justify-content: space-between; 39 | } 40 | 41 | .newDot { 42 | height: 10px; 43 | width: 10px; 44 | border-radius: 10px; 45 | flex: 1; 46 | background-color: #5cc3e8; 47 | margin-left: 5px; 48 | } -------------------------------------------------------------------------------- /app/components/MessengerCard/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import MatchMessengerBlock from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/MessengerInput/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * MessengerInput 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import styles from './styles.css'; 9 | 10 | class MessengerInput extends React.Component { // eslint-disable-line react/prefer-stateless-function 11 | constructor() { 12 | super(); 13 | this.state = { 14 | inputField: '', 15 | }; 16 | } 17 | 18 | handleKeyPress(e) { 19 | if (e.key === 'Enter' && this.state.inputField) { 20 | const id = this.props.sendTo; 21 | const message = this.state.inputField; 22 | this.props.sendMessage(id, message); 23 | this.setState({ inputField: '' }); 24 | } 25 | } 26 | 27 | render() { 28 | return ( 29 |
    30 |
    31 | { this.setState({ inputField: e.target.value }); }} onKeyPress={(e) => this.handleKeyPress(e)} contentEditable="true" className={styles.input} type="text" disabled={this.props.disabled} /> 32 | 33 |
    34 |
    35 | ); 36 | } 37 | } 38 | 39 | MessengerInput.propTypes = { 40 | sendTo: PropTypes.string, 41 | sendMessage: PropTypes.func.isRequired, 42 | disabled: PropTypes.bool, 43 | sendToName: PropTypes.string, 44 | }; 45 | 46 | export default MessengerInput; 47 | -------------------------------------------------------------------------------- /app/components/MessengerInput/styles.css: -------------------------------------------------------------------------------- 1 | .write { 2 | font-size: 16px; 3 | flex: 1; 4 | height: 40px; 5 | padding: 0 10px; 6 | color: #1A1A1A; 7 | border: 0; 8 | outline: none; 9 | background-color: #ECEFF1; 10 | font-weight: 400; 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | 15 | .writeContainer { 16 | flex: 1; 17 | display: flex; 18 | } 19 | 20 | .input { 21 | font-size: 16px; 22 | float: left; 23 | width: 100%; 24 | height: 40px; 25 | padding: 0 10px; 26 | color: #1A1A1A; 27 | border: 0; 28 | outline: none; 29 | background-color: #ECEFF1; 30 | font-weight: 400; 31 | } 32 | 33 | .input:focus { 34 | outline: none; 35 | } 36 | 37 | .send { 38 | display: inline-block; 39 | float: right; 40 | width: 20px; 41 | height: 42px; 42 | margin-left: 11px; 43 | content: ''; 44 | background-image: url('http://s30.postimg.org/nz9dho0pp/send.png'); 45 | background-repeat: no-repeat; 46 | background-position: center; 47 | cursor: pointer; 48 | } 49 | -------------------------------------------------------------------------------- /app/components/MessengerInput/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import MessengerInput from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/Panel/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import styles from './styles.css'; 3 | 4 | import Text from 'components/Text'; 5 | 6 | import tinderCardFemale from 'static/tinder_female.png'; 7 | import tinderCardMale from 'static/tinder_male.png'; 8 | 9 | const placeholderMapping = { 10 | 1: tinderCardFemale, 11 | 0: tinderCardMale, 12 | '-1': tinderCardMale, 13 | }; 14 | 15 | function renderPlaceholderMessage(isFetching, hasMatches) { 16 | if (isFetching) { 17 | return "Hold on, we're loading your recommendations!"; 18 | } 19 | if (hasMatches) { 20 | return 'Pick a person to find out more information!'; 21 | } 22 | return "We're having some trouble loading your recommendations. Check back again later!"; 23 | } 24 | 25 | const Panel = ({ targetGender, hasMatches, isFetching }) => 26 | (
    27 | 28 | {renderPlaceholderMessage(isFetching, hasMatches)} 29 |
    ); 30 | 31 | Panel.propTypes = { 32 | targetGender: PropTypes.number, 33 | hasMatches: PropTypes.bool, 34 | isFetching: PropTypes.bool, 35 | }; 36 | 37 | export default Panel; 38 | 39 | -------------------------------------------------------------------------------- /app/components/Panel/styles.css: -------------------------------------------------------------------------------- 1 | .detailView_placeholder { 2 | flex: 1; 3 | align-self: center; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | } -------------------------------------------------------------------------------- /app/components/Text/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import styles from './styles.css'; 3 | 4 | const Text = (props) => { 5 | if (!props.children) { 6 | return null; 7 | } 8 | const { className } = props; 9 | return ({props.children}); 10 | }; 11 | 12 | Text.propTypes = { 13 | type: PropTypes.string, 14 | children: PropTypes.node, 15 | }; 16 | 17 | export default Text; 18 | -------------------------------------------------------------------------------- /app/components/Text/styles.css: -------------------------------------------------------------------------------- 1 | .age { 2 | text-transform: uppercase; 3 | font-weight: 700; 4 | margin: 0; 5 | font-size: 12px; 6 | color: white; 7 | } 8 | 9 | .detail { 10 | font-weight: 600; 11 | text-transform: uppercase; 12 | font-size: 10px; 13 | color: white; 14 | } 15 | 16 | .distance { 17 | font-size: 12px; 18 | margin-left: 10px; 19 | font-weight: 400; 20 | text-transform: lowercase; 21 | } 22 | 23 | .name { 24 | color: #FFF; 25 | margin: 0; 26 | font-size: 30px; 27 | font-weight: 600; 28 | } 29 | 30 | .profileHeader { 31 | flex: 1; 32 | margin-top: 15px; 33 | text-transform: uppercase; 34 | font-size: 12px; 35 | font-weight: 600; 36 | } 37 | 38 | .placeholder { 39 | align-self: center; 40 | font-size: 12px; 41 | font-weight: 600; 42 | } 43 | 44 | .connectionName { 45 | font-size: 10px; 46 | flex: 1; 47 | text-align: center; 48 | margin-top: 5px; 49 | } 50 | 51 | .bio { 52 | color: black; 53 | display: block; 54 | font-size: 12px; 55 | margin-bottom: 10px; 56 | margin-top: 10px; 57 | } 58 | 59 | .commonInterest { 60 | font-size: 12px; 61 | font-weight: 500; 62 | padding: 5px 15px; 63 | border: 2px solid #DB4C2C; 64 | color: #DB4C2C; 65 | border-radius: 15px; 66 | margin-right: 5px; 67 | } 68 | 69 | .school { 70 | display: block; 71 | font-size: 10px; 72 | text-transform: uppercase; 73 | font-weight: 500; 74 | } 75 | 76 | .jobs { 77 | display: block; 78 | font-size: 12px; 79 | } 80 | 81 | .dropdownText { 82 | color: white; 83 | flex: 2; 84 | } 85 | 86 | .profileName { 87 | font-size: 40px; 88 | } 89 | 90 | .bioInputTextCount { 91 | float: right; 92 | } 93 | 94 | .matchName { 95 | display: flex; 96 | align-items: center; 97 | font-weight: 400; 98 | font-size: 14px; 99 | } 100 | 101 | .matchRecentMessage { 102 | display: block; 103 | font-size: 14px; 104 | color: #AAA; 105 | max-width: 200px; 106 | } 107 | 108 | .matchActivity { 109 | text-align: right; 110 | color: #AAA; 111 | font-size: 12px; 112 | } 113 | 114 | .dashboardSettingsHeader { 115 | font-size: 16px; 116 | font-weight: 800; 117 | margin: 20px 10px; 118 | display: block; 119 | } 120 | 121 | .dismiss { 122 | font-size: 12px; 123 | color: white; 124 | align-self: flex-end; 125 | flex: 1; 126 | text-align: right; 127 | } 128 | 129 | .dashboardSettingsSubheader { 130 | composes: matchRecentMessage; 131 | max-width: none; 132 | } 133 | 134 | .switchText { 135 | composes: dashboardSettingsHeader; 136 | font-weight: 300; 137 | margin: 0; 138 | font-size: 14px; 139 | margin-left: 10px; 140 | } 141 | 142 | .radioButtonLabel { 143 | composes: matchRecentMessage; 144 | margin: 0 5px; 145 | } 146 | 147 | .dashboardSettingsHeaderPhoto { 148 | composes: dashboardSettingsHeader; 149 | font-weight: 300; 150 | text-align: center; 151 | display: flex; 152 | flex: 1; 153 | flex-direction: column; 154 | justify-content: center; 155 | } 156 | 157 | .potentialLike { 158 | composes: age; 159 | color: white; 160 | font-size: 10px; 161 | background-color: #FF6966; 162 | display: inline; 163 | padding: 6px; 164 | border-radius: 3px; 165 | position: relative; 166 | bottom: 5px; 167 | left: 5px; 168 | } 169 | 170 | .historyAction { 171 | font-size: 14px; 172 | } 173 | 174 | .historyDate { 175 | margin-left: 10px; 176 | font-size: 12px; 177 | } 178 | -------------------------------------------------------------------------------- /app/containers/App/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * App.react.js 4 | * 5 | * This component is the skeleton around the actual pages, and should only 6 | * contain code that should be seen on all pages. (e.g. navigation bar) 7 | * 8 | * NOTE: while this component should technically be a stateless functional 9 | * component (SFC), hot reloading does not currently support SFCs. If hot 10 | * reloading is not a neccessity for you then you can refactor it and remove 11 | * the linting exception. 12 | */ 13 | 14 | import React from 'react'; 15 | import Navigation from 'containers/Navigation'; 16 | import Helmet from 'react-helmet'; 17 | import styles from './styles.css'; 18 | import { connect } from 'react-redux'; 19 | import { createStructuredSelector } from 'reselect'; 20 | import { selectNewNotifications } from 'containers/Messages/selectors'; 21 | 22 | class App extends React.Component { // eslint-disable-line react/prefer-stateless-function 23 | render() { 24 | return ( 25 |
    26 | 29 | 30 |
    31 | {React.Children.toArray(this.props.children)} 32 |
    33 |
    34 | ); 35 | } 36 | } 37 | 38 | const mapStateToProps = createStructuredSelector({ 39 | newNotifications: selectNewNotifications(), 40 | }); 41 | 42 | App.propTypes = { 43 | newNotifications: React.PropTypes.array, 44 | children: React.PropTypes.node.isRequired, 45 | }; 46 | 47 | export default connect(mapStateToProps, null)(App); -------------------------------------------------------------------------------- /app/containers/App/selectors.js: -------------------------------------------------------------------------------- 1 | // selectLocationState expects a plain JS object for the routing state 2 | const selectLocationState = () => { 3 | let prevRoutingState; 4 | let prevRoutingStateJS; 5 | 6 | return (state) => { 7 | const routingState = state.get('route'); // or state.route 8 | 9 | if (!routingState.equals(prevRoutingState)) { 10 | prevRoutingState = routingState; 11 | prevRoutingStateJS = routingState.toJS(); 12 | } 13 | 14 | return prevRoutingStateJS; 15 | }; 16 | }; 17 | 18 | export { 19 | selectLocationState, 20 | }; 21 | -------------------------------------------------------------------------------- /app/containers/App/styles.css: -------------------------------------------------------------------------------- 1 | /** 2 | * styles.css 3 | * 4 | * App container styles 5 | */ 6 | 7 | body{ 8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 9 | } 10 | 11 | .container { 12 | height:100vh; 13 | flex-direction: column; 14 | display: flex; 15 | } 16 | 17 | .mainContainer{ 18 | display:flex; 19 | flex:20; 20 | max-width: 100vw; 21 | } -------------------------------------------------------------------------------- /app/containers/App/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import expect from 'expect'; 3 | 4 | import { selectLocationState } from 'containers/App/selectors'; 5 | 6 | describe('selectLocationState', () => { 7 | it('should select the route as a plain JS object', () => { 8 | const route = fromJS({ 9 | locationBeforeTransitions: null, 10 | }); 11 | const mockedState = fromJS({ 12 | route, 13 | }); 14 | expect(selectLocationState()(mockedState)).toEqual(route.toJS()); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /app/containers/Auth/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOGIN_FACEBOOK, 3 | LOGIN_FACEBOOK_SUCCESS, 4 | LOGIN_FACEBOOK_ERROR, 5 | LOGIN_LOCAL, 6 | LOGIN_LOCAL_SUCCESS, 7 | SET_GLOBALS, 8 | LOGIN_CHROME, 9 | LOGIN_CHROME_SUCCESS, 10 | LOGIN_CHROME_ERROR, 11 | EMPTY_REDUCER, 12 | } from './constants'; 13 | 14 | export function emptyReducer() { 15 | return { 16 | type: EMPTY_REDUCER, 17 | }; 18 | } 19 | 20 | export function setGlobals(data) { 21 | return { 22 | type: SET_GLOBALS, 23 | payload: data, 24 | }; 25 | } 26 | 27 | export function loginLocal() { 28 | return { 29 | type: LOGIN_LOCAL, 30 | }; 31 | } 32 | 33 | export function loginLocalSuccess(token) { 34 | return { 35 | type: LOGIN_LOCAL_SUCCESS, 36 | payload: token, 37 | }; 38 | } 39 | 40 | export function loginFacebook() { 41 | return { 42 | type: LOGIN_FACEBOOK, 43 | }; 44 | } 45 | 46 | export function loginChrome(token) { 47 | return { 48 | type: LOGIN_CHROME, 49 | payload: token, 50 | }; 51 | } 52 | 53 | export function loginChromeSuccess({ authToken, fbToken }) { 54 | return { 55 | type: LOGIN_CHROME_SUCCESS, 56 | payload: { 57 | authToken, 58 | fbToken, 59 | }, 60 | }; 61 | } 62 | 63 | export function loginChromeError(err) { 64 | return { 65 | type: LOGIN_CHROME_ERROR, 66 | payload: err, 67 | }; 68 | } 69 | export function loginFacebookError(error) { 70 | return { 71 | type: LOGIN_FACEBOOK_ERROR, 72 | payload: error, 73 | }; 74 | } 75 | 76 | export function loginFacebookSuccess({ authToken, fbToken }) { 77 | return { 78 | type: LOGIN_FACEBOOK_SUCCESS, 79 | payload: { 80 | authToken, 81 | fbToken, 82 | }, 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /app/containers/Auth/constants.js: -------------------------------------------------------------------------------- 1 | export const LOGIN_FACEBOOK = 'LOGIN_FACEBOOK'; 2 | export const LOGIN_FACEBOOK_SUCCESS = 'LOGIN_FACEBOOK_SUCCESS'; 3 | export const LOGIN_FACEBOOK_ERROR = 'LOGIN_FACEBOOK_ERROR'; 4 | 5 | export const LOGIN_CHROME = 'LOGIN_CHROME'; 6 | export const LOGIN_CHROME_SUCCESS = 'LOGIN_CHROME_SUCCESS'; 7 | export const LOGIN_CHROME_ERROR = 'LOGIN_CHROME_ERROR'; 8 | 9 | export const LOGIN_LOCAL = 'LOGIN_LOCAL'; 10 | export const LOGIN_LOCAL_SUCCESS = 'LOGIN_LOCAL_SUCCESS'; 11 | 12 | export const SET_GLOBALS = 'SET_GLOBALS'; 13 | export const EMPTY_REDUCER = 'EMPTY_REDUCER'; 14 | -------------------------------------------------------------------------------- /app/containers/Auth/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { push } from 'react-router-redux'; 3 | import { connect } from 'react-redux'; 4 | import { changeLogin, changePassword, loginFacebook, loginLocal } from './actions'; 5 | import { createStructuredSelector } from 'reselect'; 6 | 7 | import { 8 | selectPassword, 9 | selectLogin, 10 | selectAuthToken, 11 | } from './selectors'; 12 | 13 | class Auth extends React.Component { 14 | 15 | componentWillMount() { 16 | if (this.props.token) { 17 | this.props.routeToDashboard(); 18 | } 19 | this.props.loginLocal(); 20 | } 21 | 22 | render() { 23 | return ( 24 |
    25 |
    26 | ); 27 | } 28 | } 29 | 30 | Auth.propTypes = { 31 | onLogin: PropTypes.func.isRequired, 32 | loginLocal: PropTypes.func.isRequired, 33 | routeToDashboard: PropTypes.func.isRequired, 34 | token: PropTypes.string, 35 | }; 36 | 37 | 38 | function mapDispatchToProps(dispatch) { 39 | return { 40 | dispatch, 41 | loginLocal: () => dispatch(loginLocal()), 42 | onLogin: (e) => { 43 | e.preventDefault(); 44 | dispatch(loginFacebook()); 45 | }, 46 | routeToDashboard: () => dispatch(push('/dashboard')), 47 | }; 48 | } 49 | 50 | const mapStateToProps = createStructuredSelector({ 51 | token: selectAuthToken(), 52 | }); 53 | 54 | 55 | export default connect(mapStateToProps, mapDispatchToProps)(Auth); 56 | -------------------------------------------------------------------------------- /app/containers/Auth/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOGIN_FACEBOOK, 3 | LOGIN_FACEBOOK_ERROR, 4 | LOGIN_FACEBOOK_SUCCESS, 5 | LOGIN_CHROME, 6 | LOGIN_CHROME_SUCCESS, 7 | LOGIN_CHROME_ERROR, 8 | SET_GLOBALS, 9 | EMPTY_REDUCER, 10 | } from './constants'; 11 | import { fromJS } from 'immutable'; 12 | 13 | // The initial state of the App 14 | const initialState = fromJS({ 15 | login: '', 16 | password: '', 17 | userToken: '', 18 | fbToken: '', 19 | authError: '', 20 | isAuthing: false, 21 | }); 22 | 23 | function authReducer(state = initialState, action) { 24 | switch (action.type) { 25 | case LOGIN_FACEBOOK: 26 | return state 27 | .set('isAuthing', true); 28 | case LOGIN_FACEBOOK_SUCCESS: 29 | return state 30 | .set('userToken', action.payload.authToken) 31 | .set('fbToken', action.payload.fbToken) 32 | .set('isAuthing', false); 33 | case LOGIN_FACEBOOK_ERROR: 34 | return state 35 | .set('authError', action.payload) 36 | .set('isAuthing', false); 37 | case LOGIN_CHROME: 38 | return state 39 | .set('isAuthing', true); 40 | case LOGIN_CHROME_SUCCESS: 41 | return state 42 | .set('userToken', action.payload.authToken) 43 | .set('fbToken', action.payload.fbToken) 44 | .set('isAuthing', false); 45 | case LOGIN_CHROME_ERROR: 46 | return state 47 | .set('authError', action.payload) 48 | .set('isAuthing', false); 49 | case SET_GLOBALS: 50 | return state.set('globals', action.payload); 51 | case EMPTY_REDUCER: 52 | return initialState; 53 | default: 54 | return state; 55 | } 56 | } 57 | 58 | export default authReducer; 59 | -------------------------------------------------------------------------------- /app/containers/Auth/sagas.js: -------------------------------------------------------------------------------- 1 | import { take, call, put, select, fork, cancel } from 'redux-saga/effects'; 2 | import { takeLatest } from 'redux-saga'; 3 | import { LOCATION_CHANGE, push } from 'react-router-redux'; 4 | import { LOGIN_FACEBOOK, LOGIN_LOCAL, LOGIN_CHROME } from './constants'; 5 | import { loginFacebookSuccess, loginFacebookError } from './actions'; 6 | import { postRequest } from 'utils/request'; 7 | import { storeToken, getToken } from 'utils/storage'; 8 | import { AUTH_URL } from 'global_constants'; 9 | import { selectLogin, selectPassword } from './selectors'; 10 | 11 | const CHROME_EXTENSION_ID = 'ijolldjdhcdcceonmopahocncafnlike'; 12 | 13 | function* storeTokensSaga({ authToken, fbToken }) { 14 | yield storeToken('tinderToken', authToken); 15 | yield storeToken('fbToken', fbToken); 16 | } 17 | 18 | function* loginFacebookSaga() { 19 | const login = yield select(selectLogin()); 20 | const password = yield select(selectPassword()); 21 | const requestURL = `${AUTH_URL}/auth/facebook`; 22 | const body = { 23 | login, 24 | password, 25 | }; 26 | try { 27 | const authData = yield call(postRequest, requestURL, body); 28 | if (authData.status === 200) { 29 | yield call(storeTokensSaga, authData.data); 30 | yield put(loginFacebookSuccess({ authToken: authData.data.authToken, fbToken: authData.data.fbToken })); 31 | yield put(push('/dashboard/home')); 32 | } 33 | } catch (loginError) { 34 | yield put(loginFacebookError(loginError)); 35 | } 36 | } 37 | 38 | function* loginChromeSaga(token) { 39 | const requestURL = `${AUTH_URL}/auth/facebook/${token}`; 40 | try { 41 | const authData = yield call(postRequest, requestURL); 42 | if (authData.status === 200) { 43 | yield call(storeTokensSaga, authData.data); 44 | yield put(loginFacebookSuccess({ authToken: authData.data.authToken, fbToken: authData.data.fbToken })); 45 | yield put(push('/dashboard/home')); 46 | } 47 | } catch (loginError) { 48 | yield put(loginFacebookError(loginError)); 49 | } 50 | } 51 | 52 | function* loginLocalSaga() { 53 | const authToken = yield getToken('tinderToken'); 54 | const token = window.location.pathname.split('/login/')[1]; 55 | if (token) { 56 | yield loginChromeSaga(token); 57 | } else { 58 | try { 59 | const authenticationData = yield call(postRequest, `${AUTH_URL}/tinder/checkAuth`, { authToken }); 60 | if (authenticationData.data) { 61 | yield put(loginFacebookSuccess({ authToken })); 62 | yield put(push('/dashboard/home')); 63 | } 64 | } catch (err) { 65 | chrome.runtime.sendMessage(CHROME_EXTENSION_ID, { type: 'doAuth' }); // eslint-disable-line 66 | } 67 | } 68 | } 69 | 70 | 71 | export function* authSaga() { 72 | const watcher = [ 73 | yield fork(takeLatest, LOGIN_FACEBOOK, loginFacebookSaga), 74 | yield fork(takeLatest, LOGIN_LOCAL, loginLocalSaga), 75 | yield fork(takeLatest, LOGIN_CHROME, loginChromeSaga), 76 | ]; 77 | 78 | yield take(LOCATION_CHANGE); 79 | yield watcher.map(each => cancel(each)); 80 | } 81 | 82 | export default [ 83 | authSaga, 84 | ]; 85 | -------------------------------------------------------------------------------- /app/containers/Auth/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | const selectAuth = () => (state) => state.get('auth'); 4 | 5 | const selectLogin = () => createSelector( 6 | selectAuth(), 7 | (authState) => authState.get('login') 8 | ); 9 | 10 | const selectPassword = () => createSelector( 11 | selectAuth(), 12 | (authState) => authState.get('password') 13 | ); 14 | 15 | const selectFacebookToken = () => createSelector( 16 | selectAuth(), 17 | (authState) => authState.get('fbToken') 18 | ); 19 | 20 | const selectAuthToken = () => createSelector( 21 | selectAuth(), 22 | (authState) => { 23 | if (!authState) { 24 | return undefined; 25 | } 26 | return authState.get('userToken'); 27 | } 28 | ); 29 | 30 | const selectId = () => createSelector( 31 | selectAuth(), 32 | (authState) => authState.get('userId') 33 | ); 34 | 35 | export { 36 | selectAuth, 37 | selectLogin, 38 | selectPassword, 39 | selectId, 40 | selectFacebookToken, 41 | selectAuthToken, 42 | }; 43 | -------------------------------------------------------------------------------- /app/containers/Dashboard/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Dashboard actions 4 | * 5 | */ 6 | 7 | import { 8 | FETCH_DATA, 9 | FETCH_DATA_ERROR, 10 | FETCH_DATA_SUCCESS, 11 | FETCH_UPDATES, 12 | FETCH_UPDATES_ERROR, 13 | FETCH_UPDATES_END, 14 | FETCHED_RECOMMENDATIONS_WITH_PREFS, 15 | STORE_META_DATA_SUCCESS, 16 | REHYDRATE_MATCHES, 17 | REHYDRATE_MATCHES_SUCCESS, 18 | REHYDRATE_MATCHES_ERROR, 19 | CHECK_NOTIFICATION_PERMISSIONS, 20 | UPDATE_ACTIONS_REDUCER, 21 | } from './constants'; 22 | 23 | export function updateActionsReducer(data) { 24 | return { 25 | type: UPDATE_ACTIONS_REDUCER, 26 | payload: data, 27 | }; 28 | } 29 | 30 | export function checkNotificationPermissions() { 31 | return { 32 | type: CHECK_NOTIFICATION_PERMISSIONS, 33 | }; 34 | } 35 | 36 | export function rehydrateMatches() { 37 | return { 38 | type: REHYDRATE_MATCHES, 39 | }; 40 | } 41 | 42 | export function rehydrateMatchesSuccess() { 43 | return { 44 | type: REHYDRATE_MATCHES_SUCCESS, 45 | }; 46 | } 47 | 48 | export function rehydrateMatchesError() { 49 | return { 50 | type: REHYDRATE_MATCHES_ERROR, 51 | }; 52 | } 53 | 54 | export function storeMetadataSuccess() { 55 | return { 56 | type: STORE_META_DATA_SUCCESS, 57 | }; 58 | } 59 | 60 | export function fetchData(dataType) { 61 | return { 62 | type: FETCH_DATA, 63 | payload: dataType, 64 | }; 65 | } 66 | 67 | export function fetchDataError(error) { 68 | return { 69 | type: FETCH_DATA_ERROR, 70 | payload: error, 71 | }; 72 | } 73 | 74 | export function fetchDataSuccess(dataType, data) { 75 | return { 76 | type: FETCH_DATA_SUCCESS, 77 | payload: { 78 | dataType, 79 | data, 80 | }, 81 | }; 82 | } 83 | 84 | export function fetchUpdates() { 85 | return { 86 | type: FETCH_UPDATES, 87 | }; 88 | } 89 | 90 | export function fetchUpdatesError(errors) { 91 | return { 92 | type: FETCH_UPDATES_ERROR, 93 | payload: errors, 94 | }; 95 | } 96 | 97 | export function fetchUpdatesEnd() { 98 | return { 99 | type: FETCH_UPDATES_END, 100 | }; 101 | } 102 | 103 | export function fetchedRecommendationsWithPrefs() { 104 | return { 105 | type: FETCHED_RECOMMENDATIONS_WITH_PREFS, 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /app/containers/Dashboard/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Dashboard constants 4 | * 5 | */ 6 | 7 | export const FETCH_UPDATES = 'FETCH_UPDATES'; 8 | export const FETCH_UPDATES_ERROR = 'FETCH_UPDATES_ERROR'; 9 | export const FETCH_UPDATES_END = 'FETCH_UPDATES_END'; 10 | 11 | export const FETCH_DATA = 'FETCH_DATA'; 12 | export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS'; 13 | export const FETCH_DATA_ERROR = 'FETCH_DATA_ERROR'; 14 | 15 | export const FETCHED_RECOMMENDATIONS_WITH_PREFS = 'FETCHED_RECOMMENDATIONS_WITH_PREFS'; 16 | export const STORE_META_DATA_SUCCESS = 'STORE_META_DATA_SUCCESS'; 17 | export const REHYDRATE_MATCHES = 'REHYDRATE_MATCHES'; 18 | export const REHYDRATE_MATCHES_SUCCESS = 'REHYDRATE_MATCHES_SUCCESS'; 19 | export const REHYDRATE_MATCHES_ERROR = 'REHYDRATE_MATCHES_ERROR'; 20 | 21 | export const CHECK_NOTIFICATION_PERMISSIONS = 'CHECK_NOTIFICATION_PERMISSIONS'; 22 | export const UPDATE_ACTIONS_REDUCER = 'UPDATE_ACTIONS_REDUCER'; 23 | -------------------------------------------------------------------------------- /app/containers/Dashboard/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Dashboard 4 | * 5 | */ 6 | 7 | import React, { PropTypes } from 'react'; 8 | import { connect } from 'react-redux'; 9 | import { checkNotificationPermissions } from './actions'; 10 | import { selectAuthToken } from 'containers/Auth/selectors'; 11 | import { createStructuredSelector } from 'reselect'; 12 | 13 | import Notification from 'containers/Notification'; 14 | import styles from './styles.css'; 15 | 16 | export class Dashboard extends React.Component { // eslint-disable-line react/prefer-stateless-function 17 | componentWillMount() { 18 | const { authToken } = this.props; 19 | 20 | if (!authToken) { 21 | localStorage.setItem('routeIntent', window.location.pathname); 22 | window.location.replace('/login'); 23 | } 24 | } 25 | 26 | componentDidMount() { 27 | this.props.checkNotificationPermissions(); 28 | } 29 | 30 | render() { 31 | return ( 32 |
    33 | 34 | {this.props.children} 35 |
    36 | ); 37 | } 38 | } 39 | 40 | Dashboard.propTypes = { 41 | children: PropTypes.node.isRequired, 42 | checkNotificationPermissions: PropTypes.func, 43 | authToken: PropTypes.string, 44 | }; 45 | 46 | function mapDispatchToProps(dispatch) { 47 | return { 48 | checkNotificationPermissions: () => dispatch(checkNotificationPermissions()), 49 | }; 50 | } 51 | 52 | const mapStateToProps = createStructuredSelector({ 53 | authToken: selectAuthToken(), 54 | }); 55 | 56 | 57 | export default connect(mapStateToProps, mapDispatchToProps)(Dashboard); 58 | -------------------------------------------------------------------------------- /app/containers/Dashboard/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Dashboard reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | FETCH_UPDATES, 10 | FETCH_UPDATES_END, 11 | FETCH_UPDATES_ERROR, 12 | FETCH_DATA, 13 | FETCH_DATA_ERROR, 14 | FETCH_DATA_SUCCESS, 15 | FETCHED_RECOMMENDATIONS_WITH_PREFS, 16 | UPDATE_ACTIONS_REDUCER, 17 | } from './constants'; 18 | 19 | import { 20 | EDITING_BIO, 21 | SET_AGE_FILTER, 22 | SET_DISTANCE_FILTER, 23 | SET_GENDER, 24 | SET_GENDER_FILTER, 25 | REORDER_PHOTOS, 26 | SET_DISCOVER, 27 | SELECT_LOCATION, 28 | } from 'containers/MainDashboard/constants'; 29 | 30 | import { LOCATION_CHANGE } from 'react-router-redux'; 31 | 32 | const initialState = fromJS({ 33 | user: false, 34 | rating: false, 35 | history: false, 36 | actionsHistory: [], 37 | recommendations: false, 38 | shouldUpdateRecommendations: true, 39 | lastError: false, 40 | updates: [], 41 | isFetching: false, 42 | isSyncing: false, 43 | }); 44 | 45 | 46 | function dashboardReducer(state = initialState, action) { 47 | switch (action.type) { 48 | case FETCH_DATA: 49 | return state.set('isFetching', true); 50 | case FETCH_DATA_ERROR: 51 | return state 52 | .set('lastError', action.payload) 53 | .set('isFetching', false); 54 | case FETCH_DATA_SUCCESS: 55 | return state.set(action.payload.dataType, fromJS(action.payload.data)); 56 | case FETCH_UPDATES: 57 | return state.set('isSyncing', true); 58 | case FETCH_UPDATES_ERROR: 59 | return state 60 | .set('lastError', action.payload); 61 | case FETCH_UPDATES_END: 62 | return state 63 | .set('isSyncing', false); 64 | case UPDATE_ACTIONS_REDUCER: 65 | return state.set('actionsHistory', action.payload); 66 | case EDITING_BIO: 67 | return state.setIn(['user', 'bio'], action.payload); 68 | case SET_DISCOVER: 69 | return state 70 | .set('shouldUpdateRecommendations', true) 71 | .setIn(['user', 'discoverable'], action.payload.discovery); 72 | case SET_AGE_FILTER: 73 | return state 74 | .set('shouldUpdateRecommendations', true) 75 | .setIn(['user', 'age_filter_max'], action.payload.age_filter_max) 76 | .setIn(['user', 'age_filter_min'], action.payload.age_filter_min); 77 | case SET_DISTANCE_FILTER: 78 | return state 79 | .set('shouldUpdateRecommendations', true) 80 | .setIn(['user', 'distance_filter'], action.payload.distance_filter); 81 | case SET_GENDER_FILTER: 82 | return state 83 | .set('shouldUpdateRecommendations', true) 84 | .setIn(['user', 'gender_filter'], action.payload.gender_filter); 85 | case SET_GENDER: 86 | return state 87 | .set('shouldUpdateRecommendations', true) 88 | .setIn(['user', 'gender'], action.payload.gender); 89 | case REORDER_PHOTOS: 90 | return state.setIn(['user', 'photos'], action.payload); 91 | case FETCHED_RECOMMENDATIONS_WITH_PREFS: 92 | return state.set('shouldUpdateRecommendations', false); 93 | case SELECT_LOCATION: 94 | return state.set('shouldUpdateRecommendations', true); 95 | case LOCATION_CHANGE: 96 | return state.set('isFetching', false); 97 | default: 98 | return state; 99 | } 100 | } 101 | 102 | export default dashboardReducer; 103 | -------------------------------------------------------------------------------- /app/containers/Dashboard/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the dashboard state domain 5 | */ 6 | const selectDashboardDomain = () => state => state.get('dashboard'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | 13 | /** 14 | * Default selector used by Dashboard 15 | */ 16 | 17 | const selectDashboard = () => createSelector( 18 | selectDashboardDomain(), 19 | (substate) => { 20 | if (!substate) { 21 | return undefined; 22 | } 23 | return substate.toJS(); 24 | } 25 | ); 26 | 27 | const selectFetching = () => createSelector( 28 | selectDashboard(), 29 | (dashboardState) => dashboardState.isFetching || false 30 | ); 31 | 32 | const selectIsSyncing = () => createSelector( 33 | selectDashboard(), 34 | (state) => state.isSyncing, 35 | ); 36 | 37 | const selectTargetGender = () => createSelector( 38 | selectDashboard(), 39 | (dashboardState) => dashboardState.user.gender_filter 40 | ); 41 | 42 | const selectUserObject = () => createSelector( 43 | selectDashboard(), 44 | (dashboardState) => { 45 | if (!dashboardState) { 46 | return undefined; 47 | } 48 | return dashboardState.user; 49 | } 50 | ); 51 | 52 | const selectUserID = () => createSelector( 53 | selectUserObject(), 54 | (substate) => substate._id // eslint-disable-line 55 | ); 56 | 57 | const selectDashboardHistory = () => createSelector( 58 | selectDashboard(), 59 | (dashboard) => dashboard.history 60 | ); 61 | 62 | const selectMatchesHistory = () => createSelector( 63 | selectDashboard(), 64 | (dashboard) => dashboard.matches 65 | ); 66 | 67 | const selectUserName = () => createSelector( 68 | selectUserObject(), 69 | (userObject) => { 70 | if (!userObject) return undefined; 71 | return userObject.name; 72 | } 73 | ); 74 | 75 | const selectActionsHistory = () => createSelector( 76 | selectDashboard(), 77 | (state) => state.actionsHistory 78 | ); 79 | 80 | export default selectDashboard; 81 | export { 82 | selectDashboard, 83 | selectDashboardDomain, 84 | selectTargetGender, 85 | selectFetching, 86 | selectUserObject, 87 | selectUserID, 88 | selectDashboardHistory, 89 | selectMatchesHistory, 90 | selectUserName, 91 | selectIsSyncing, 92 | selectActionsHistory, 93 | }; 94 | -------------------------------------------------------------------------------- /app/containers/Dashboard/styles.css: -------------------------------------------------------------------------------- 1 | .dashboard { /* stylelint-ignore */ 2 | display:flex; 3 | flex-basis: 100%; 4 | flex-direction: row; 5 | max-width: 100%; 6 | } -------------------------------------------------------------------------------- /app/containers/Dashboard/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | defaultAction, 4 | } from '../actions'; 5 | import { 6 | DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('Dashboard actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: DEFAULT_ACTION, 14 | }; 15 | expect(defaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/containers/Dashboard/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import Dashboard from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/Dashboard/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import dashboardReducer from '../reducer'; 3 | import { fromJS } from 'immutable'; 4 | 5 | describe('dashboardReducer', () => { 6 | it('returns the initial state', () => { 7 | expect(dashboardReducer(undefined, {})).toEqual(fromJS({})); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /app/containers/Dashboard/tests/sagas.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | import expect from 'expect'; 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../sagas'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/containers/Dashboard/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | // import { selectDashboardDomain } from '../selectors'; 2 | // import { fromJS } from 'immutable'; 3 | import expect from 'expect'; 4 | 5 | // const selector = selectDashboardDomain(); 6 | 7 | describe('selectDashboardDomain', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect('Test case').toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/HomePage/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HomePage 3 | * 4 | * This is the first thing users see of our App, at the '/' route 5 | * 6 | * NOTE: while this component should technically be a stateless functional 7 | * component (SFC), hot reloading does not currently support SFCs. If hot 8 | * reloading is not a neccessity for you then you can refactor it and remove 9 | * the linting exception. 10 | */ 11 | 12 | import React from 'react'; 13 | import { FormattedMessage } from 'react-intl'; 14 | import messages from './messages'; 15 | 16 | export default class HomePage extends React.Component { // eslint-disable-line react/prefer-stateless-function 17 | 18 | render() { 19 | return ( 20 |

    21 | 22 |

    23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/containers/HomePage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HomePage Messages 3 | * 4 | * This contains all the text for the HomePage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.HomePage.header', 11 | defaultMessage: 'This is HomePage components !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider actions 4 | * 5 | */ 6 | 7 | import { 8 | CHANGE_LOCALE, 9 | } from './constants'; 10 | 11 | export function changeLocale(languageLocale) { 12 | return { 13 | type: CHANGE_LOCALE, 14 | locale: languageLocale, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider constants 4 | * 5 | */ 6 | 7 | export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE'; 8 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider 4 | * 5 | * this component connects the redux state language locale to the 6 | * IntlProvider component and i18n messages (loaded from `app/translations`) 7 | */ 8 | 9 | import React from 'react'; 10 | import { connect } from 'react-redux'; 11 | import { createSelector } from 'reselect'; 12 | import { IntlProvider } from 'react-intl'; 13 | import { selectLocale } from './selectors'; 14 | 15 | export class LanguageProvider extends React.Component { // eslint-disable-line react/prefer-stateless-function 16 | render() { 17 | return ( 18 | 19 | {React.Children.only(this.props.children)} 20 | 21 | ); 22 | } 23 | } 24 | 25 | LanguageProvider.propTypes = { 26 | locale: React.PropTypes.string, 27 | messages: React.PropTypes.object, 28 | children: React.PropTypes.element.isRequired, 29 | }; 30 | 31 | 32 | const mapStateToProps = createSelector( 33 | selectLocale(), 34 | (locale) => ({ locale }) 35 | ); 36 | 37 | function mapDispatchToProps(dispatch) { 38 | return { 39 | dispatch, 40 | }; 41 | } 42 | 43 | export default connect(mapStateToProps, mapDispatchToProps)(LanguageProvider); 44 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | CHANGE_LOCALE, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({ 13 | locale: 'en', 14 | }); 15 | 16 | function languageProviderReducer(state = initialState, action) { 17 | switch (action.type) { 18 | case CHANGE_LOCALE: 19 | return state 20 | .set('locale', action.locale); 21 | default: 22 | return state; 23 | } 24 | } 25 | 26 | export default languageProviderReducer; 27 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the languageToggle state domain 5 | */ 6 | const selectLanguage = () => state => state.get('language'); 7 | 8 | /** 9 | * Select the language locale 10 | */ 11 | 12 | const selectLocale = () => createSelector( 13 | selectLanguage(), 14 | (languageState) => languageState.get('locale') 15 | ); 16 | 17 | export { 18 | selectLanguage, 19 | selectLocale, 20 | }; 21 | -------------------------------------------------------------------------------- /app/containers/MainDashboard/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | EDITING_BIO, 3 | REORDER_PHOTOS, 4 | SET_AGE_FILTER, 5 | SET_GENDER_FILTER, 6 | SET_DISTANCE_FILTER, 7 | SET_GENDER, 8 | SET_DISCOVER, 9 | SELECTING_LOCATION, 10 | SELECT_LOCATION, 11 | CLEAR_LOCAL_DATA, 12 | LOG_OUT, 13 | } from './constants'; 14 | 15 | export function setDiscover(newValue) { 16 | return { 17 | type: SET_DISCOVER, 18 | payload: { 19 | discovery: newValue, 20 | }, 21 | }; 22 | } 23 | 24 | export function setGender(newValue) { 25 | return { 26 | type: SET_GENDER, 27 | payload: { 28 | gender: newValue, 29 | }, 30 | }; 31 | } 32 | 33 | export function setGenderFilter(newValue) { 34 | return { 35 | type: SET_GENDER_FILTER, 36 | payload: { 37 | gender_filter: newValue, 38 | }, 39 | }; 40 | } 41 | 42 | export function selectingLocation() { 43 | return { 44 | type: SELECTING_LOCATION, 45 | }; 46 | } 47 | 48 | export function selectLocation(lat, lng) { 49 | return { 50 | type: SELECT_LOCATION, 51 | payload: { 52 | lat, 53 | lng, 54 | }, 55 | }; 56 | } 57 | 58 | export function editingBio(bioState) { 59 | return { 60 | type: EDITING_BIO, 61 | payload: bioState, 62 | }; 63 | } 64 | 65 | export function reorderPhotos(photos) { 66 | return { 67 | type: REORDER_PHOTOS, 68 | payload: photos, 69 | }; 70 | } 71 | 72 | export function setAgeFilter(newFilter) { 73 | return { 74 | type: SET_AGE_FILTER, 75 | payload: { 76 | age_filter_min: newFilter[0], 77 | age_filter_max: newFilter[1], 78 | }, 79 | }; 80 | } 81 | 82 | export function setDistanceFilter(newFilter) { 83 | return { 84 | type: SET_DISTANCE_FILTER, 85 | payload: { 86 | distance_filter: newFilter[0], 87 | }, 88 | }; 89 | } 90 | 91 | export function clearLocalData() { 92 | return { 93 | type: CLEAR_LOCAL_DATA, 94 | }; 95 | } 96 | 97 | export function logOut() { 98 | return { 99 | type: LOG_OUT, 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /app/containers/MainDashboard/constants.js: -------------------------------------------------------------------------------- 1 | export const SELECTING_LOCATION = 'SELECTING_LOCATION'; 2 | export const SELECT_LOCATION = 'SELECT_LOCATION'; 3 | 4 | export const EDITING_BIO = 'EDITING_BIO'; 5 | export const REORDER_PHOTOS = 'REORDER_PHOTOS'; 6 | export const SET_AGE_FILTER = 'SET_AGE_FILTER'; 7 | export const SET_GENDER_FILTER = 'SET_GENDER_FILTER'; 8 | export const SET_DISTANCE_FILTER = 'SET_DISTANCE_FILTER'; 9 | export const SET_GENDER = 'SET_GENDER'; 10 | export const SET_DISCOVER = 'SET_DISCOVER'; 11 | 12 | export const LOG_OUT = 'LOG_OUT'; 13 | export const LOG_OUT_SUCCESS = 'LOG_OUT_SUCCESS'; 14 | 15 | export const CLEAR_LOCAL_DATA = 'CLEAR_LOCAL_DATA'; 16 | export const CLEAR_LOCAL_DATA_SUCCESS = 'CLEAR_LOCAL_DATA_SUCCESS'; 17 | -------------------------------------------------------------------------------- /app/containers/MainDashboard/messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl'; 2 | 3 | export default defineMessages({ 4 | about: { 5 | id: 'tinderapp.containers.MainDashboard.about', 6 | defaultMessage: 'About', 7 | }, 8 | male: { 9 | id: 'tinderapp.containers.MainDashboard.male', 10 | defaultMessage: 'Male', 11 | }, 12 | female: { 13 | id: 'tinderapp.containers.MainDashboard.female', 14 | defaultMessage: 'Female', 15 | }, 16 | yourInterests: { 17 | id: 'tinderapp.containers.MainDashboard.yourInterests', 18 | defaultMessage: 'Your Interests', 19 | }, 20 | ageAndDistanceHeader: { 21 | id: 'tinderapp.containers.MainDashboard.ageAndDistanceHeader', 22 | defaultMessage: 'Age, Gender and distance options', 23 | }, 24 | ageAndDistanceSubheader: { 25 | id: 'tinderapp.containers.MainDashboard.ageAndDistanceSubheader', 26 | defaultMessage: 'Adjust your settings here', 27 | }, 28 | photoHeader: { 29 | id: 'tinderapp.containers.MainDashboard.photoHeader', 30 | defaultMessage: 'Your Photos', 31 | }, 32 | photoSubheader: { 33 | id: 'tinderapp.containers.MainDashboard.photoHeader', 34 | defaultMessage: 'Rearrange your images', 35 | }, 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /app/containers/MainDashboard/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Messages reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | SELECTING_LOCATION, 10 | SELECT_LOCATION, 11 | } from './constants'; 12 | 13 | const initialState = fromJS({ 14 | settingLocation: false, 15 | mapPinLocation: { 16 | lat: '', 17 | lng: '', 18 | }, 19 | }); 20 | 21 | function mainDashboardReducer(state = initialState, action) { 22 | switch (action.type) { 23 | case SELECTING_LOCATION: 24 | return state.set('settingLocation', !state.get('settingLocation')); 25 | case SELECT_LOCATION: 26 | return state.setIn(['mapPinLocation', 'lat'], action.payload.lat).setIn(['mapPinLocation', 'lng'], action.payload.lng); 27 | default: 28 | return state; 29 | } 30 | } 31 | 32 | export default mainDashboardReducer; 33 | -------------------------------------------------------------------------------- /app/containers/MainDashboard/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | const selectMainDashboardDomain = () => state => state.get('userDashboard'); 4 | 5 | /** 6 | * Other specific selectors 7 | */ 8 | 9 | 10 | /** 11 | * Default selector used by Dashboard 12 | */ 13 | 14 | const selectUserDashboard = () => createSelector( 15 | selectMainDashboardDomain(), 16 | (substate) => substate.toJS() 17 | ); 18 | 19 | const selectIsSettingLocation = () => createSelector( 20 | selectUserDashboard(), 21 | (substate) => substate.settingLocation 22 | ); 23 | 24 | const selectMarkerLocation = () => createSelector( 25 | selectUserDashboard(), 26 | (substate) => substate.mapPinLocation 27 | ); 28 | 29 | export { 30 | selectUserDashboard, 31 | selectIsSettingLocation, 32 | selectMarkerLocation, 33 | }; 34 | -------------------------------------------------------------------------------- /app/containers/Messages/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Messages actions 4 | * 5 | */ 6 | 7 | import { 8 | SELECT_PERSON, 9 | SEND_MESSAGE, 10 | SEND_MESSAGE_SUCCESS, 11 | SEND_MESSAGE_ERROR, 12 | UNMATCH, 13 | UNMATCH_SUCCESS, 14 | UNMATCH_ERROR, 15 | ALL_DATA_FETCHED, 16 | UPDATE_POINTER, 17 | FETCH_MATCHES_DATA, 18 | FETCH_MATCHES_DATA_ERROR, 19 | FETCH_MATCHES_DATA_SUCCESS, 20 | FETCH_MATCHES_LOCALLY, 21 | FETCH_MATCHES_DATA_NEW, 22 | DUMP_ALL, 23 | DUMP_ALL_SUCCESS, 24 | DUMP_ALL_INIT, 25 | SHOULD_RELOAD_DATA, 26 | RELOAD_DATA_PLEASE, 27 | PUSH_NEW_NOTIFICATION, 28 | READ_NEW_NOTIFICATION, 29 | } from './constants'; 30 | 31 | export function pushNewNotification(id) { 32 | return { 33 | type: PUSH_NEW_NOTIFICATION, 34 | payload: id, 35 | }; 36 | } 37 | 38 | export function readNewNotification(id) { 39 | return { 40 | type: READ_NEW_NOTIFICATION, 41 | payload: id, 42 | }; 43 | } 44 | 45 | export function reloadDataPlease() { 46 | return { 47 | type: RELOAD_DATA_PLEASE, 48 | }; 49 | } 50 | 51 | export function shouldReloadData() { 52 | return { 53 | type: SHOULD_RELOAD_DATA, 54 | }; 55 | } 56 | 57 | export function fetchMatchData() { 58 | return { 59 | type: FETCH_MATCHES_DATA, 60 | }; 61 | } 62 | 63 | export function fetchMatchDataSuccess(data) { 64 | return { 65 | type: FETCH_MATCHES_DATA_SUCCESS, 66 | payload: data, 67 | }; 68 | } 69 | 70 | export function fetchMatchDataError(error) { 71 | return { 72 | type: FETCH_MATCHES_DATA_ERROR, 73 | payload: error, 74 | }; 75 | } 76 | 77 | export function allDataFetched() { 78 | return { 79 | type: ALL_DATA_FETCHED, 80 | }; 81 | } 82 | 83 | export function selectPersonAction(id) { 84 | return { 85 | type: SELECT_PERSON, 86 | payload: id, 87 | }; 88 | } 89 | 90 | export function sendMessage(id, message) { 91 | return { 92 | type: SEND_MESSAGE, 93 | payload: { 94 | id, 95 | message, 96 | }, 97 | }; 98 | } 99 | 100 | export function sendMessageSuccess() { 101 | return { 102 | type: SEND_MESSAGE_SUCCESS, 103 | }; 104 | } 105 | 106 | export function sendMessageError(error) { 107 | return { 108 | type: SEND_MESSAGE_ERROR, 109 | payload: error, 110 | }; 111 | } 112 | 113 | export function unmatch(id) { 114 | return { 115 | type: UNMATCH, 116 | payload: { 117 | id, 118 | }, 119 | }; 120 | } 121 | 122 | export function unmatchSuccess() { 123 | return { 124 | type: UNMATCH_SUCCESS, 125 | }; 126 | } 127 | 128 | export function unmatchError(error) { 129 | return { 130 | type: UNMATCH_ERROR, 131 | payload: error, 132 | }; 133 | } 134 | 135 | export function updatePointer() { 136 | return { 137 | type: UPDATE_POINTER, 138 | }; 139 | } 140 | 141 | // Experimental 142 | export function dumpAll() { 143 | return { 144 | type: DUMP_ALL, 145 | }; 146 | } 147 | 148 | export function dumpAllSuccess() { 149 | return { 150 | type: DUMP_ALL_SUCCESS, 151 | }; 152 | } 153 | 154 | export function dumpAllInit() { 155 | return { 156 | type: DUMP_ALL_INIT, 157 | }; 158 | } 159 | 160 | export function fetchMatchDataLocally() { 161 | return { 162 | type: FETCH_MATCHES_LOCALLY, 163 | }; 164 | } 165 | 166 | export function fetchMatchDataUpdate(matches) { 167 | return { 168 | type: FETCH_MATCHES_DATA_NEW, 169 | payload: matches, 170 | }; 171 | } 172 | -------------------------------------------------------------------------------- /app/containers/Messages/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Messages constants 4 | * 5 | */ 6 | 7 | export const SELECT_PERSON = 'SELECT_PERSON'; 8 | export const SEND_MESSAGE = 'SEND_MESSAGE'; 9 | export const SEND_MESSAGE_SUCCESS = 'SEND_MESSAGE_SUCCESS'; 10 | export const SEND_MESSAGE_ERROR = 'SEND_MESSAGE_ERROR'; 11 | export const UNMATCH = 'UNMATCH'; 12 | export const UNMATCH_SUCCESS = 'UNMATCH_SUCCESS'; 13 | export const UNMATCH_ERROR = 'UNMATCH_ERROR'; 14 | export const UPDATE_POINTER = 'UPDATE_POINTER'; 15 | export const ALL_DATA_FETCHED = 'ALL_DATA_FETCHED'; 16 | export const FETCH_MATCHES_DATA = 'FETCH_MATCHES_DATA'; 17 | export const FETCH_MATCHES_DATA_SUCCESS = 'FETCH_MATCHES_DATA_SUCCESS'; 18 | export const FETCH_MATCHES_DATA_ERROR = 'FETCH_MATCHES_DATA_ERROR'; 19 | 20 | // Experimental 21 | export const DUMP_ALL = 'DUMP_ALL'; 22 | export const DUMP_ALL_INIT = 'DUMP_ALL_INIT'; 23 | export const DUMP_ALL_SUCCESS = 'DUMP_ALL_SUCCESS'; 24 | export const FETCH_MATCHES_LOCALLY = 'FETCH_MATCHES_LOCALLY'; 25 | export const FETCH_MATCHES_DATA_NEW = 'FETCH_MATCHES_DATA_NEW'; 26 | export const SHOULD_RELOAD_DATA = 'SHOULD_RELOAD_DATA'; 27 | export const RELOAD_DATA_PLEASE = 'RELOAD_DATA_PLEASE'; // this is for when the data has been appended and is being removed. 28 | 29 | export const PUSH_NEW_NOTIFICATION = 'PUSH_NEW_NOTIFICATION'; 30 | export const READ_NEW_NOTIFICATION = 'READ_NEW_NOTIFICATION'; 31 | -------------------------------------------------------------------------------- /app/containers/Messages/messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl'; 2 | 3 | export default defineMessages({ 4 | whenLoadingData: { 5 | id: 'tinderapp.containers.Messages.whenLoadingData', 6 | defaultMessage: "Hold on, we're syncing your matches." 7 | }, 8 | whenLoadedData: { 9 | id: 'tinderapp.containers.Messages.whenLoadedData', 10 | defaultMessage: "Select a person to start chatting!" 11 | }, 12 | whenNoDataisFound: { 13 | id: 'tinderapp.containers.Messages.whenNoDataisFound', 14 | defaultMessage: "Visit your recommendations to find a new match!" 15 | }, 16 | }); -------------------------------------------------------------------------------- /app/containers/Messages/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Messages reducer 4 | * 5 | */ 6 | 7 | import { fromJS, List } from 'immutable'; 8 | import { 9 | SELECT_PERSON, 10 | SEND_MESSAGE, 11 | SEND_MESSAGE_ERROR, 12 | SEND_MESSAGE_SUCCESS, 13 | UNMATCH, 14 | UPDATE_POINTER, 15 | ALL_DATA_FETCHED, 16 | FETCH_MATCHES_DATA, 17 | FETCH_MATCHES_DATA_ERROR, 18 | FETCH_MATCHES_DATA_SUCCESS, 19 | FETCH_MATCHES_DATA_NEW, 20 | DUMP_ALL, 21 | RELOAD_DATA_PLEASE, 22 | PUSH_NEW_NOTIFICATION, 23 | } from './constants'; 24 | 25 | import { LOCATION_CHANGE } from 'react-router-redux'; 26 | 27 | const initialState = fromJS({ 28 | pointer: 1, 29 | allMessagesFetched: false, 30 | currentPerson: '', 31 | fetchingErrors: '', 32 | isSending: false, 33 | matches: false, 34 | isFetching: false, 35 | optimisticUI: [], 36 | newMatches: [], 37 | }); 38 | 39 | function messagesReducer(state = initialState, action) { 40 | switch (action.type) { 41 | case FETCH_MATCHES_DATA: 42 | return state 43 | .set('optimisticUI', []) 44 | .set('isFetching', true); 45 | case FETCH_MATCHES_DATA_ERROR: 46 | return state 47 | .set('fetchingErrors', action.payload) 48 | .set('isFetching', false); 49 | case FETCH_MATCHES_DATA_SUCCESS: 50 | return state 51 | .set('matches', new List(action.payload)) 52 | .set('isFetching', false); 53 | case FETCH_MATCHES_DATA_NEW: 54 | return state.set('matches', action.payload.concat(state.get('matches'))); 55 | case SELECT_PERSON: 56 | return state 57 | .set('currentPerson', null) 58 | .set('currentPerson', action.payload) 59 | .set('newMatches', state.get('newMatches').filter((each) => each.id !== action.payload)); 60 | case SEND_MESSAGE: 61 | return state 62 | .set('isSending', true) 63 | .set('optimisticUI', action.payload.message.match(/gif/) ? state.get('optimisticUI') : state.get('optimisticUI').concat(action.payload)); 64 | case SEND_MESSAGE_SUCCESS: 65 | return state.set('isSending', false); 66 | case SEND_MESSAGE_ERROR: 67 | return state.set('isSending', false); 68 | case UNMATCH: 69 | return state 70 | .set('isFetching', true) 71 | .set('optimisticUI', []) 72 | .set('currentPerson', null); 73 | case UPDATE_POINTER: 74 | return state.set('pointer', state.get('pointer') + 1); 75 | case ALL_DATA_FETCHED: 76 | return state.set('allMessagesFetched', true); 77 | case DUMP_ALL: 78 | return state 79 | .set('optimisticUI', []) 80 | .set('matches', []); 81 | case LOCATION_CHANGE: 82 | return state 83 | .set('optimisticUI', []) 84 | .set('currentPerson', null); 85 | case RELOAD_DATA_PLEASE: 86 | return state 87 | .set('optimisticUI', []); 88 | case PUSH_NEW_NOTIFICATION: 89 | return state.set('newMatches', state.get('newMatches').concat(action.payload.filter((each) => each.id !== state.get('currentPerson')))); 90 | default: 91 | return state; 92 | } 93 | } 94 | 95 | export default messagesReducer; 96 | -------------------------------------------------------------------------------- /app/containers/Messages/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the messages state domain 5 | */ 6 | const selectMessagesDomain = () => state => state.get('messages'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | 13 | /** 14 | * Default selector used by Messages 15 | */ 16 | 17 | const selectMessages = () => createSelector( 18 | selectMessagesDomain(), 19 | (substate) => { 20 | if (!substate) return undefined; 21 | return substate.toJS(); 22 | } 23 | ); 24 | 25 | const selectMatches = () => createSelector( 26 | selectMessages(), 27 | (substate) => substate.matches 28 | ); 29 | 30 | const selectPersonId = () => createSelector( 31 | selectMessages(), 32 | (messages) => messages.currentPerson, 33 | ); 34 | 35 | const selectOptimisticUI = () => createSelector( 36 | selectMessages(), 37 | (messages) => messages.optimisticUI, 38 | ); 39 | 40 | const selectPersonSelector = () => createSelector( 41 | selectMatches(), 42 | selectPersonId(), 43 | (state, id) => { 44 | if (!id || id === '' || !state) { 45 | return undefined; 46 | } 47 | return state.slice().filter((each) => { 48 | if (!each.person) return false; 49 | return each.person._id === id; // eslint-disable-line no-underscore-dangle 50 | })[0]; 51 | } 52 | ); 53 | 54 | const selectMatchDetailImages = () => createSelector( 55 | selectPersonSelector(), 56 | (person) => { 57 | if (!person || person === '' || !person.person) { 58 | return undefined; 59 | } 60 | return person.person.photos.map((each) => ({ original: each.url })); 61 | } 62 | ); 63 | 64 | const selectMatchMessages = () => createSelector( 65 | selectPersonSelector(), 66 | (person) => { 67 | if (!person) { 68 | return undefined; 69 | } 70 | return person.messages.slice().map((each) => { 71 | if (person.person._id === each.to) { // eslint-disable-line no-underscore-dangle 72 | return { 73 | from: 'you', 74 | payload: each, 75 | }; 76 | } 77 | return { 78 | from: 'me', 79 | payload: each, 80 | }; 81 | }); 82 | } 83 | ); 84 | 85 | const selectPointer = () => createSelector( 86 | selectMessages(), 87 | (substate) => substate.pointer 88 | ); 89 | 90 | const selectIsAllFetched = () => createSelector( 91 | selectMessages(), 92 | (substate) => substate.allMessagesFetched 93 | ); 94 | 95 | const selectIsFetching = () => createSelector( 96 | selectMessages(), 97 | (substate) => substate.isFetching 98 | ); 99 | 100 | const selectNewNotifications = () => createSelector( 101 | selectMessages(), 102 | (substate) => { 103 | if (!substate) return undefined; 104 | return substate.newMatches.map((each) => each.id); 105 | } 106 | ); 107 | 108 | export { 109 | selectMessagesDomain, 110 | selectPersonSelector, 111 | selectPersonId, 112 | selectMatchDetailImages, 113 | selectMatchMessages, 114 | selectOptimisticUI, 115 | selectPointer, 116 | selectIsAllFetched, 117 | selectIsFetching, 118 | selectMatches, 119 | selectNewNotifications, 120 | }; 121 | -------------------------------------------------------------------------------- /app/containers/Messages/styles.css: -------------------------------------------------------------------------------- 1 | .messagesContainer { /* stylelint-ignore */ 2 | display: flex; 3 | width: 100vw; 4 | height: 100%; 5 | } 6 | 7 | .messagePanel { 8 | flex: 3; 9 | border-right:1px solid #eee; 10 | overflow-y: scroll; 11 | } 12 | 13 | .messagePanel::-webkit-scrollbar, messagePanelContainer::-webkit-scrollbar { width: 0 !important } 14 | 15 | .messengerPanel { 16 | flex: 10; 17 | } 18 | 19 | .messengerPanelContainer { 20 | display: flex; 21 | flex-direction: column; 22 | height: 100%; 23 | } 24 | 25 | .horizontalMessengerPanel { 26 | display: flex; 27 | flex: 4; 28 | } 29 | 30 | .columnMessengerPanel { 31 | flex: 4; 32 | display: flex; 33 | flex-direction: column; 34 | } 35 | 36 | .profileBioPanel { 37 | flex: 2; 38 | border-left: 1px solid #eee; 39 | display: flex; 40 | } 41 | 42 | .messagesPanel { 43 | flex: 4; 44 | min-height: 450px; 45 | padding: 20px; 46 | overflow-y: scroll; 47 | display: flex; 48 | flex-direction: column; 49 | } 50 | 51 | .chatBoxPanel { 52 | flex: 0.5; 53 | /*border-top: 1px solid #eee;*/ 54 | display: flex; 55 | padding: 20px; 56 | align-items: center; 57 | } 58 | 59 | .matchBlock { 60 | display: flex; 61 | flex: 1; 62 | max-height: 100px; 63 | border-bottom: 1px solid #eee; 64 | cursor: pointer; 65 | } 66 | 67 | .conversationPlaceholderImage { 68 | max-height: 200px; 69 | align-self: center; 70 | } -------------------------------------------------------------------------------- /app/containers/Messages/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | defaultAction, 4 | } from '../actions'; 5 | import { 6 | DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('Messages actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: DEFAULT_ACTION, 14 | }; 15 | expect(defaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /app/containers/Messages/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import Messages from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/Messages/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import messagesReducer from '../reducer'; 3 | import { fromJS } from 'immutable'; 4 | 5 | describe('messagesReducer', () => { 6 | it('returns the initial state', () => { 7 | expect(messagesReducer(undefined, {})).toEqual(fromJS({})); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /app/containers/Messages/tests/sagas.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | import expect from 'expect'; 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../sagas'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/containers/Messages/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | // import { selectMessagesDomain } from '../selectors'; 2 | // import { fromJS } from 'immutable'; 3 | import expect from 'expect'; 4 | 5 | // const selector = selectMessagesDomain(); 6 | 7 | describe('selectMessagesDomain', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect('Test case').toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/Navigation/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import styles from './styles.css'; 3 | import { connect } from 'react-redux'; 4 | import { createStructuredSelector } from 'reselect'; 5 | import { selectNewNotifications } from 'containers/Messages/selectors'; 6 | import { selectUserName } from 'containers/Dashboard/selectors'; 7 | import { selectAuthToken } from 'containers/Auth/selectors'; 8 | 9 | import { push } from 'react-router-redux'; 10 | 11 | 12 | class Navigation extends React.Component { // eslint-disable-line 13 | render() { 14 | return (this.props.isLoggedIn ? 15 |
    16 | {this.props.userName || 'Dashboard'} 17 | Recommendations 18 | Matches{this.props.notifications && this.props.notifications.length ?
    : null} 19 |
    :
    20 | ); 21 | } 22 | } 23 | 24 | Navigation.propTypes = { 25 | navigateTo: PropTypes.func.isRequired, 26 | notifications: PropTypes.array, 27 | userName: PropTypes.string, 28 | isLoggedIn: PropTypes.string, 29 | }; 30 | 31 | function mapDispatchToProps(dispatch) { 32 | return { 33 | navigateTo: (event) => { 34 | if (window.location.pathname !== event.target.id) { 35 | dispatch(push(event.target.id)); 36 | } 37 | }, 38 | }; 39 | } 40 | 41 | const mapStateToProps = createStructuredSelector({ 42 | notifications: selectNewNotifications(), 43 | userName: selectUserName(), 44 | isLoggedIn: selectAuthToken(), 45 | }); 46 | 47 | export default connect(mapStateToProps, mapDispatchToProps)(Navigation); 48 | -------------------------------------------------------------------------------- /app/containers/Navigation/styles.css: -------------------------------------------------------------------------------- 1 | .navigation{ 2 | display: flex; 3 | flex:1; 4 | min-height: 60px; 5 | border-bottom:1px solid #e2e2e2; 6 | flex-direction: row; 7 | justify-content: flex-start; 8 | align-items: center; 9 | } 10 | 11 | .commonInterest { 12 | font-size: 12px; 13 | font-weight: 500; 14 | padding: 5px 15px; 15 | border: 2px solid #DB4C2C; 16 | color: #DB4C2C; 17 | border-radius: 15px; 18 | margin-right: 5px; 19 | } 20 | 21 | .navigation a{ 22 | text-decoration: none; 23 | color:#eee; 24 | } 25 | 26 | .navigation_item{ 27 | flex-basis: 10%; 28 | cursor: pointer; 29 | color: #e2e2e2; 30 | text-align: center; 31 | display: flex; 32 | justify-content: center; 33 | flex-direction: row; 34 | height: 100%; 35 | padding: 20px; 36 | } 37 | 38 | .navigation_item_active { 39 | composes: navigation_item; 40 | color:#DB4C2C !important; 41 | cursor: pointer; 42 | border-bottom: 3px solid #DB4C2C; 43 | } 44 | 45 | .navigation_item:hover { 46 | color: #DB4C2C; 47 | border-bottom: 3px solid #DB4C2C; 48 | } 49 | 50 | 51 | .newDot { 52 | height: 8px; 53 | max-width: 8px; 54 | border-radius: 8px; 55 | margin-left: 5px; 56 | background-color: #DB4C2C; 57 | flex: 1; 58 | align-self: flex-start; 59 | } -------------------------------------------------------------------------------- /app/containers/NotFoundPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NotFoundPage 3 | * 4 | * This is the page we show when the user visits a url that doesn't have a route 5 | * 6 | * NOTE: while this component should technically be a stateless functional 7 | * component (SFC), hot reloading does not currently support SFCs. If hot 8 | * reloading is not a neccessity for you then you can refactor it and remove 9 | * the linting exception. 10 | */ 11 | 12 | import React from 'react'; 13 | import { FormattedMessage } from 'react-intl'; 14 | import messages from './messages'; 15 | 16 | export default class NotFound extends React.Component { // eslint-disable-line react/prefer-stateless-function 17 | 18 | render() { 19 | return ( 20 |

    21 | 22 |

    23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotFoundPage Messages 3 | * 4 | * This contains all the text for the NotFoundPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.NotFoundPage.header', 11 | defaultMessage: 'This is NotFoundPage component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/Notification/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | GLOBAL_NOTIFICATION_ADDED, 3 | GLOBAL_NOTIFICATION_HANDLED, 4 | GLOBAL_NOTIFICATION_PUSHED, 5 | GLOBAL_NOTIFICATION_RECEIVED, 6 | NOTIFICATION_MANUAL_REMOVE, 7 | } from './constants'; 8 | 9 | export function dismissNotification() { 10 | return { 11 | type: NOTIFICATION_MANUAL_REMOVE, 12 | }; 13 | } 14 | 15 | export function newNotification(data) { 16 | return { 17 | type: GLOBAL_NOTIFICATION_ADDED, 18 | payload: data, 19 | }; 20 | } 21 | 22 | export function handledNotification() { 23 | return { 24 | type: GLOBAL_NOTIFICATION_HANDLED, 25 | }; 26 | } 27 | 28 | export function pushNotification(data) { 29 | return { 30 | type: GLOBAL_NOTIFICATION_PUSHED, 31 | payload: data, 32 | }; 33 | } 34 | 35 | export function newNotificationAdded() { 36 | return { 37 | type: GLOBAL_NOTIFICATION_RECEIVED, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /app/containers/Notification/constants.js: -------------------------------------------------------------------------------- 1 | export const GLOBAL_NOTIFICATION_RECEIVED = 'GLOBAL_NOTIFICATION_RECEIVED'; 2 | export const GLOBAL_NOTIFICATION_PUSHED = 'GLOBAL_NOTIFICATION_PUSHED'; 3 | export const GLOBAL_NOTIFICATION_HANDLED = 'GLOBAL_NOTIFICATION_HANDLED'; 4 | export const GLOBAL_NOTIFICATION_ADDED = 'GLOBAL_NOTIFICATION_ADDED'; 5 | export const NOTIFICATION_MANUAL_REMOVE = 'NOTIFICATION_MANUAL_REMOVE'; 6 | -------------------------------------------------------------------------------- /app/containers/Notification/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createStructuredSelector } from 'reselect'; 4 | import { selectCurrentError } from 'containers/Notification/selectors'; 5 | import { dismissNotification } from './actions'; 6 | 7 | import styles from './styles.css'; 8 | import Button from 'components/Button'; 9 | import Text from 'components/Text'; 10 | 11 | const dropdownStyleMapping = { 12 | error: { 13 | backgroundColor: '#e95f5c', 14 | }, 15 | match: { 16 | backgroundColor: '#5cc3e8', 17 | }, 18 | }; 19 | 20 | class Dropdown extends React.Component { // eslint-disable-line 21 | constructor(props) { 22 | super(props); 23 | this.renderDropdown = this.renderDropdown.bind(this); 24 | } 25 | 26 | renderDropdown(currentNotification) { 27 | let notificationString; 28 | let type; 29 | if (currentNotification.toString().match(/liking/)) { 30 | notificationString = currentNotification; 31 | type = 'error'; 32 | } else if (currentNotification.toString().match(/400|network|trouble|timeout/i)) { 33 | notificationString = "We're having some trouble connecting to the server."; 34 | type = 'error'; 35 | } else if (currentNotification.toString().match(/match/i)) { 36 | notificationString = currentNotification.toString(); 37 | type = 'match'; 38 | } 39 | return ( 40 |
    41 |
    42 | {notificationString} 43 |
    44 |
    45 | ); 46 | } 47 | 48 | render() { 49 | return this.props.currentError ? this.renderDropdown(this.props.currentError) : null; 50 | } 51 | } 52 | 53 | Dropdown.propTypes = { 54 | currentError: React.PropTypes.oneOfType([ 55 | React.PropTypes.string, 56 | React.PropTypes.object, 57 | ]), 58 | dismissNotification: React.PropTypes.func, 59 | }; 60 | 61 | const mapStateToProps = createStructuredSelector({ 62 | currentError: selectCurrentError(), 63 | }); 64 | 65 | function mapDispatchToProps(dispatch) { 66 | return { 67 | dismissNotification: () => dispatch(dismissNotification()), 68 | }; 69 | } 70 | 71 | 72 | export default connect(mapStateToProps, mapDispatchToProps)(Dropdown); 73 | -------------------------------------------------------------------------------- /app/containers/Notification/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | GLOBAL_NOTIFICATION_HANDLED, 3 | GLOBAL_NOTIFICATION_RECEIVED, 4 | GLOBAL_NOTIFICATION_PUSHED, 5 | GLOBAL_NOTIFICATION_ADDED, 6 | } from './constants'; 7 | import { fromJS } from 'immutable'; 8 | import { LOCATION_CHANGE } from 'react-router-redux'; 9 | 10 | // The initial state of the App 11 | const initialState = fromJS({ 12 | globalNotifications: [], 13 | currentMessage: '', 14 | }); 15 | 16 | function authReducer(state = initialState, action) { 17 | switch (action.type) { 18 | case GLOBAL_NOTIFICATION_RECEIVED: 19 | return state 20 | .set('globalErrors', state.get('globalNotifications').concat(action.payload)); 21 | case GLOBAL_NOTIFICATION_HANDLED: 22 | return state 23 | .set('globalErrors', state.get('globalNotifications').shift()) 24 | .set('currentMessage', ''); 25 | case GLOBAL_NOTIFICATION_PUSHED: 26 | return state.set('currentMessage', action.payload); 27 | case GLOBAL_NOTIFICATION_ADDED: 28 | return state; 29 | case LOCATION_CHANGE: 30 | return state.set('currentMessage', ''); 31 | default: 32 | return state; 33 | } 34 | } 35 | 36 | export default authReducer; 37 | -------------------------------------------------------------------------------- /app/containers/Notification/sagas.js: -------------------------------------------------------------------------------- 1 | import { delay } from 'redux-saga'; 2 | import { take, call, put, actionChannel, fork, race, cancel } from 'redux-saga/effects'; 3 | import { LOCATION_CHANGE } from 'react-router-redux'; 4 | 5 | import { 6 | pushNotification, 7 | handledNotification, 8 | } from './actions'; 9 | 10 | import { 11 | GLOBAL_NOTIFICATION_ADDED, 12 | NOTIFICATION_MANUAL_REMOVE, 13 | } from './constants'; 14 | 15 | function* notificationHandler(errorData) { 16 | yield put(pushNotification(errorData)); 17 | yield race({ 18 | cancel: take(NOTIFICATION_MANUAL_REMOVE), 19 | timeOut: call(delay, 4000), 20 | }); 21 | yield put(handledNotification()); 22 | } 23 | 24 | function* notificationWatcher() { 25 | const notificationsChannel = yield actionChannel(GLOBAL_NOTIFICATION_ADDED); 26 | while (true) { 27 | const { payload } = yield take(notificationsChannel); 28 | yield call(notificationHandler, payload); 29 | } 30 | } 31 | 32 | export function* notificationSaga() { 33 | const notifications = yield fork(notificationWatcher); 34 | 35 | yield take(LOCATION_CHANGE); 36 | yield cancel(notifications); 37 | } 38 | 39 | export default [ 40 | notificationSaga, 41 | ]; 42 | -------------------------------------------------------------------------------- /app/containers/Notification/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | const selectNotifications = () => state => state.get('notifications').toJS(); 4 | 5 | export const selectCurrentError = () => createSelector( 6 | selectNotifications(), 7 | (notificationsState) => notificationsState.currentMessage 8 | ); 9 | -------------------------------------------------------------------------------- /app/containers/Notification/styles.css: -------------------------------------------------------------------------------- 1 | .dropdownContainer { 2 | flex: 1; 3 | display: flex; 4 | margin: 10px; 5 | height: 100%; 6 | flex-direction: row; 7 | } 8 | 9 | .errorDropdown { 10 | position: fixed; 11 | border-radius: 3px; 12 | top: 0; 13 | right: 0; 14 | margin: 30px; 15 | padding: 10px; 16 | display: block; 17 | width: 400px; 18 | z-index: 10; 19 | transition: 0.3s; 20 | display: flex; 21 | flex-direction: column; 22 | } 23 | 24 | /*.errorDropdown:hover { 25 | height: 50px; 26 | display: block; 27 | transition: 0.3s; 28 | border-bottom: 1px solid #db4c2c; 29 | }*/ -------------------------------------------------------------------------------- /app/containers/Recommendations/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | LIKE_PERSON, 3 | LIKE_PERSON_SUCCESS, 4 | LIKE_PERSON_ERROR, 5 | PASS_PERSON, 6 | PASS_PERSON_SUCCESS, 7 | PASS_PERSON_ERROR, 8 | DETAIL_PERSON, 9 | SUPERLIKE_PERSON, 10 | SUPERLIKE_PERSON_SUCCESS, 11 | SUPERLIKE_PERSON_ERROR, 12 | FETCH_RECOMMENDATIONS, 13 | FETCH_RECOMMENDATIONS_ERROR, 14 | FETCH_RECOMMENDATIONS_SUCCESS, 15 | FETCH_RECOMMENDATIONS_LOCALLY, 16 | SORT_RECOMMENDATIONS, 17 | REMOVE_RECOMMENDATION, 18 | DUMP_ALL_RECOMMENDATIONS, 19 | DUMP_ALL_RECOMMENDATIONS_START, 20 | DUMP_ALL_RECOMMENDATIONS_SUCCESS, 21 | SORT_LIKES, 22 | NEW_MATCH, 23 | } from './constants'; 24 | 25 | export function newMatch(details, id) { 26 | return { 27 | type: NEW_MATCH, 28 | id, 29 | details: { 30 | date: new Date().toISOString(), 31 | name: details, 32 | }, 33 | }; 34 | } 35 | 36 | export function superLikePerson(id, hash, details) { 37 | return { 38 | type: SUPERLIKE_PERSON, 39 | id, 40 | hash, 41 | details: { 42 | ...details, 43 | date: new Date().toISOString(), 44 | }, 45 | }; 46 | } 47 | 48 | export function superLikePersonSuccess(data) { 49 | return { 50 | type: SUPERLIKE_PERSON_SUCCESS, 51 | payload: data, 52 | }; 53 | } 54 | 55 | export function superLikePersonError(errors) { 56 | return { 57 | type: SUPERLIKE_PERSON_ERROR, 58 | errors, 59 | }; 60 | } 61 | 62 | export function likePerson(id, hash, details) { 63 | return { 64 | type: LIKE_PERSON, 65 | id, 66 | hash, 67 | details: { 68 | ...details, 69 | date: new Date().toISOString(), 70 | }, 71 | }; 72 | } 73 | 74 | export function likePersonSuccess(data) { 75 | return { 76 | type: LIKE_PERSON_SUCCESS, 77 | payload: data, 78 | }; 79 | } 80 | 81 | export function likePersonError(errors) { 82 | return { 83 | type: LIKE_PERSON_ERROR, 84 | errors, 85 | }; 86 | } 87 | 88 | export function passPerson(id, hash, details) { 89 | return { 90 | type: PASS_PERSON, 91 | id, 92 | hash, 93 | details: { 94 | ...details, 95 | date: new Date().toISOString(), 96 | }, 97 | }; 98 | } 99 | 100 | export function passPersonSuccess(data) { 101 | return { 102 | type: PASS_PERSON_SUCCESS, 103 | payload: data, 104 | }; 105 | } 106 | 107 | export function passPersonError(errors) { 108 | return { 109 | type: PASS_PERSON_ERROR, 110 | errors, 111 | }; 112 | } 113 | 114 | export function detailPerson(id, image) { 115 | return { 116 | type: DETAIL_PERSON, 117 | id, 118 | image, 119 | }; 120 | } 121 | 122 | export function fetchRecommendations() { 123 | return { 124 | type: FETCH_RECOMMENDATIONS, 125 | }; 126 | } 127 | 128 | export function fetchRecommendationsLocally() { 129 | return { 130 | type: FETCH_RECOMMENDATIONS_LOCALLY, 131 | }; 132 | } 133 | 134 | export function fetchRecommendationsSuccess(data) { 135 | return { 136 | type: FETCH_RECOMMENDATIONS_SUCCESS, 137 | payload: data, 138 | }; 139 | } 140 | 141 | export function fetchRecommendationsError(errors) { 142 | return { 143 | type: FETCH_RECOMMENDATIONS_ERROR, 144 | payload: errors, 145 | }; 146 | } 147 | 148 | export function sortLikes(data) { 149 | return { 150 | type: SORT_LIKES, 151 | payload: data, 152 | }; 153 | } 154 | 155 | export function sortRecommendations(sortType) { 156 | return { 157 | type: SORT_RECOMMENDATIONS, 158 | payload: sortType, 159 | }; 160 | } 161 | 162 | export function removeRecommendation(id) { 163 | return { 164 | type: REMOVE_RECOMMENDATION, 165 | payload: id, 166 | }; 167 | } 168 | 169 | export function dumpAllRecommendationsStart() { 170 | return { 171 | type: DUMP_ALL_RECOMMENDATIONS_START, 172 | }; 173 | } 174 | 175 | export function dumpAllRecommendations() { 176 | return { 177 | type: DUMP_ALL_RECOMMENDATIONS, 178 | }; 179 | } 180 | 181 | export function dumpAllRecommendationsSuccess() { 182 | return { 183 | type: DUMP_ALL_RECOMMENDATIONS_SUCCESS, 184 | }; 185 | } 186 | 187 | -------------------------------------------------------------------------------- /app/containers/Recommendations/constants.js: -------------------------------------------------------------------------------- 1 | export const LIKE_PERSON = 'LIKE_PERSON'; 2 | export const LIKE_PERSON_SUCCESS = 'LIKE_PERSON_SUCCESS'; 3 | export const LIKE_PERSON_ERROR = 'LIKE_PERSON_ERROR'; 4 | 5 | export const SUPERLIKE_PERSON = 'SUPERLIKE_PERSON'; 6 | export const SUPERLIKE_PERSON_SUCCESS = 'SUPERLIKE_PERSON_SUCCESS'; 7 | export const SUPERLIKE_PERSON_ERROR = 'SUPERLIKE_PERSON_ERROR'; 8 | 9 | export const PASS_PERSON = 'PASS_PERSON'; 10 | export const PASS_PERSON_SUCCESS = 'PASS_PERSON_SUCCESS'; 11 | export const PASS_PERSON_ERROR = 'PASS_PERSON_ERROR'; 12 | 13 | export const FETCH_RECOMMENDATIONS = 'FETCH_RECOMMENDATIONS'; 14 | export const FETCH_RECOMMENDATIONS_SUCCESS = 'FETCH_RECOMMENDATIONS_SUCCESS'; 15 | export const FETCH_RECOMMENDATIONS_ERROR = 'FETCH_RECOMMENDATIONS_ERROR'; 16 | export const FETCH_RECOMMENDATIONS_LOCALLY = 'FETCH_RECOMMENDATIONS_LOCALLY'; 17 | 18 | export const DUMP_ALL_RECOMMENDATIONS_START = 'DUMP_ALL_RECOMMENDATIONS_START'; 19 | export const DUMP_ALL_RECOMMENDATIONS = 'DUMP_ALL_RECOMMENDATIONS'; 20 | export const DUMP_ALL_RECOMMENDATIONS_SUCCESS = 'DUMP_ALL_RECOMMENDATIONS_SUCCESS'; 21 | 22 | export const DETAIL_PERSON = 'DETAIL_PERSON'; 23 | export const SORT_LIKES = 'SORT_LIKES'; 24 | export const SORT_RECOMMENDATIONS = 'SORT_RECOMMENDATIONS'; 25 | export const REMOVE_RECOMMENDATION = 'REMOVE_RECOMMENDATION'; 26 | export const NEW_MATCH = 'NEW_MATCH'; -------------------------------------------------------------------------------- /app/containers/Recommendations/messages.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litdevelopers/tinder/6eb771d682ef7f5ea537105556f3733ee1019502/app/containers/Recommendations/messages.js -------------------------------------------------------------------------------- /app/containers/Recommendations/reducer.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import { LOCATION_CHANGE } from 'react-router-redux'; 3 | import { 4 | SUPERLIKE_PERSON, 5 | PASS_PERSON, 6 | LIKE_PERSON, 7 | SUPERLIKE_PERSON_SUCCESS, 8 | SUPERLIKE_PERSON_ERROR, 9 | LIKE_PERSON_SUCCESS, 10 | LIKE_PERSON_ERROR, 11 | PASS_PERSON_SUCCESS, 12 | PASS_PERSON_ERROR, 13 | DETAIL_PERSON, 14 | SORT_RECOMMENDATIONS, 15 | FETCH_RECOMMENDATIONS, 16 | FETCH_RECOMMENDATIONS_ERROR, 17 | FETCH_RECOMMENDATIONS_SUCCESS, 18 | REMOVE_RECOMMENDATION, 19 | DUMP_ALL_RECOMMENDATIONS, 20 | FETCH_RECOMMENDATIONS_LOCALLY, 21 | SORT_LIKES, 22 | NEW_MATCH, 23 | } from './constants'; 24 | 25 | import { 26 | matchesSortByDistance, 27 | matchesSortByYoungest, 28 | matchesSortByOldest, 29 | } from 'utils/operations'; 30 | 31 | const sortMapping = { 32 | distance: matchesSortByDistance, 33 | youngest: matchesSortByYoungest, 34 | oldest: matchesSortByOldest, 35 | }; 36 | 37 | const initialState = fromJS({ 38 | currentDetailView: { 39 | id: '', 40 | image: '', 41 | }, 42 | lastError: '', 43 | isFetching: false, 44 | recommendations: false, 45 | lastAction: '', 46 | sortLikes: new Set(), 47 | }); 48 | 49 | export default function recommendationsReducer(state = initialState, action) { 50 | switch (action.type) { 51 | case DETAIL_PERSON: 52 | return state 53 | .setIn(['currentDetailView', 'id'], action.id); 54 | case LIKE_PERSON: 55 | case PASS_PERSON: 56 | case SUPERLIKE_PERSON: 57 | case FETCH_RECOMMENDATIONS_LOCALLY: 58 | return state.set('isFetching', true); 59 | case LIKE_PERSON_SUCCESS: 60 | case PASS_PERSON_SUCCESS: 61 | case SUPERLIKE_PERSON_SUCCESS: 62 | return state 63 | .set('isFetching', false) 64 | .set('lastAction', action.payload); 65 | case LIKE_PERSON_ERROR: 66 | case SUPERLIKE_PERSON_ERROR: 67 | case PASS_PERSON_ERROR: 68 | return state 69 | .set('lastError', action.payload) 70 | .set('isFetching', false); 71 | case FETCH_RECOMMENDATIONS: 72 | return state.set('isFetching', true); 73 | case FETCH_RECOMMENDATIONS_SUCCESS: 74 | return state 75 | .set('recommendations', action.payload) 76 | .set('isFetching', false); 77 | case FETCH_RECOMMENDATIONS_ERROR: 78 | return state 79 | .set('isFetching', false) 80 | .set('lastError', action.payload); 81 | case SORT_RECOMMENDATIONS: 82 | return state.set('recommendations', action.payload === 'normal' ? state.get('recommendations') : state.get('recommendations').splice(0).sort(sortMapping[action.payload])); 83 | case REMOVE_RECOMMENDATION: 84 | return state 85 | .set('recommendations', state.get('recommendations').filter((each) => each._id !== action.payload)); 86 | case DUMP_ALL_RECOMMENDATIONS: 87 | return state.set('recommendations', false); 88 | case SORT_LIKES: 89 | return state 90 | .set('sortLikes', new Set([...state.get('sortLikes'), ...new Set(action.payload)])); 91 | case NEW_MATCH: 92 | return state.set('lastAction', action.data); 93 | case LOCATION_CHANGE: 94 | return initialState; 95 | default: 96 | return state; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/containers/Recommendations/selectors.js: -------------------------------------------------------------------------------- 1 | /* eslint no-underscore-dangle: 1 */ 2 | 3 | import { createSelector } from 'reselect'; 4 | import { selectDashboard } from 'containers/Dashboard/selectors'; 5 | import { mergeArray } from 'utils/operations'; 6 | 7 | const selectRecommendationsDomain = () => state => state.get('recommendations'); 8 | const selectRecommendations = () => createSelector( 9 | selectRecommendationsDomain(), 10 | (substate) => substate.toJS() 11 | ); 12 | 13 | const selectRecommendationsList = () => createSelector( 14 | selectRecommendations(), 15 | (dashboardState) => dashboardState.recommendations 16 | ); 17 | 18 | const selectPotentialMatchList = () => createSelector( 19 | selectRecommendations(), 20 | (dashboardState) => { 21 | const matches = Array.from(dashboardState.sortLikes).map((each) => each._id); 22 | return matches; 23 | } 24 | ); 25 | 26 | const selectLimitedRecommendationsList = () => createSelector( 27 | selectRecommendations(), 28 | (substate) => { 29 | if (substate.recommendations) { 30 | return substate.recommendations.slice(0, 20); 31 | } 32 | return null; 33 | } 34 | ); 35 | 36 | const selectCurrentID = () => createSelector( 37 | selectRecommendations(), 38 | (substate) => substate.currentDetailView.id 39 | ); 40 | 41 | const selectCurrentRecommendation = () => createSelector( 42 | selectRecommendationsList(), 43 | selectCurrentID(), 44 | (matches, currentID) => { 45 | if (!matches) return undefined; 46 | return matches.filter((each) => each._id === currentID)[0]; 47 | } 48 | ); 49 | 50 | const selectCurrentRecommendationsLinks = () => createSelector( 51 | selectCurrentRecommendation(), 52 | (currentMatchState) => { 53 | if (!currentMatchState || currentMatchState === '') { 54 | return undefined; 55 | } 56 | const tinderImages = currentMatchState.photos.map((each) => each.url); 57 | const instagramImages = (currentMatchState.instagram && currentMatchState.instagram.photos) ? currentMatchState.instagram.photos.map((each) => each.image) : []; 58 | return mergeArray(tinderImages, instagramImages, 20, (each) => { return { original: each }; }); 59 | } 60 | ); 61 | 62 | const selectIsFetching = () => createSelector( 63 | selectRecommendations(), 64 | (substate) => substate.isFetching 65 | ); 66 | 67 | const selectShouldUpdate = () => createSelector( 68 | selectDashboard(), 69 | (substate) => substate.shouldUpdateRecommendations 70 | ); 71 | 72 | export { 73 | selectRecommendationsDomain, 74 | selectRecommendationsList, 75 | selectLimitedRecommendationsList, 76 | selectCurrentRecommendation, 77 | selectCurrentRecommendationsLinks, 78 | selectIsFetching, 79 | selectShouldUpdate, 80 | selectPotentialMatchList, 81 | selectCurrentID, 82 | }; 83 | -------------------------------------------------------------------------------- /app/containers/Recommendations/styles.css: -------------------------------------------------------------------------------- 1 | .dashboardMatchesContainer{ 2 | display:flex; 3 | flex-basis: 100%; 4 | flex-direction: row; 5 | max-width: 100%; 6 | } 7 | 8 | .dashboardMatchesCards { 9 | display: flex; 10 | flex: 8; 11 | flex-direction: column; 12 | } 13 | 14 | 15 | .dashboardMatchesDetails { 16 | border-left:1px solid #eee; 17 | display: flex; 18 | flex-direction: column; 19 | flex: 2.75; 20 | max-width: 400px !important; 21 | min-width: 400px; 22 | } 23 | 24 | 25 | .dashboardMatchesCardsContainer{ 26 | overflow-y: scroll; 27 | display: flex; 28 | flex-direction: row; 29 | flex-wrap: wrap; 30 | flex: 15; 31 | margin-left: 15px; 32 | max-width: 100vw; 33 | padding-top: 20px; 34 | } 35 | 36 | 37 | .dashboardMatchesCardsContainer > div{ 38 | display: flex; 39 | flex-direction: row; 40 | flex-wrap: wrap; 41 | flex: 15; 42 | } 43 | 44 | .dashboardMatchesNavigation { 45 | background-color: white; 46 | border-bottom: 1px solid #eee; 47 | flex:1; 48 | display: flex; 49 | flex-direction: row; 50 | align-items: center; 51 | justify-content: space-between; 52 | padding: 20px; 53 | width: 100%; 54 | } 55 | 56 | .dashboardMatchesContainerButtons { 57 | display: flex; 58 | justify-content: space-around; 59 | align-content: center; 60 | } 61 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litdevelopers/tinder/6eb771d682ef7f5ea537105556f3733ee1019502/app/favicon.ico -------------------------------------------------------------------------------- /app/global_constants.js: -------------------------------------------------------------------------------- 1 | export const AUTH_URL = 'http://localhost:3000/api'; 2 | -------------------------------------------------------------------------------- /app/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * i18n.js 3 | * 4 | * This will setup the i18n language files and locale data for your app. 5 | * 6 | */ 7 | import { addLocaleData } from 'react-intl'; 8 | 9 | import enLocaleData from 'react-intl/locale-data/en'; 10 | 11 | export const appLocales = [ 12 | 'en', 13 | ]; 14 | 15 | import enTranslationMessages from './translations/en.json'; 16 | 17 | addLocaleData(enLocaleData); 18 | 19 | const formatTranslationMessages = (messages) => { 20 | const formattedMessages = {}; 21 | for (const message of messages) { 22 | formattedMessages[message.id] = message.message || message.defaultMessage; 23 | } 24 | 25 | return formattedMessages; 26 | }; 27 | 28 | export const translationMessages = { 29 | en: formatTranslationMessages(enTranslationMessages), 30 | }; 31 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | React.js Boilerplate 12 | 13 | 14 | 15 |
    16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Boilerplate", 3 | "icons": [ 4 | { 5 | "src": "favicon.png", 6 | "sizes": "48x48", 7 | "type": "image/png", 8 | "density": 1.0 9 | }, 10 | { 11 | "src": "favicon.png", 12 | "sizes": "96x96", 13 | "type": "image/png", 14 | "density": 2.0 15 | }, 16 | { 17 | "src": "favicon.png", 18 | "sizes": "144x144", 19 | "type": "image/png", 20 | "density": 3.0 21 | }, 22 | { 23 | "src": "favicon.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "density": 4.0 27 | } 28 | ], 29 | "start_url": "index.html", 30 | "display": "standalone", 31 | "orientation": "portrait", 32 | "background_color": "#FFFFFF" 33 | } -------------------------------------------------------------------------------- /app/reducers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine all reducers in this file and export the combined reducers. 3 | * If we were to do this in store.js, reducers wouldn't be hot reloadable. 4 | */ 5 | 6 | import { combineReducers } from 'redux-immutable'; 7 | import { fromJS } from 'immutable'; 8 | import { LOCATION_CHANGE } from 'react-router-redux'; 9 | import languageProviderReducer from 'containers/LanguageProvider/reducer'; 10 | 11 | /* 12 | * routeReducer 13 | * 14 | * The reducer merges route location changes into our immutable state. 15 | * The change is necessitated by moving to react-router-redux@4 16 | * 17 | */ 18 | 19 | // Initial routing state 20 | const routeInitialState = fromJS({ 21 | locationBeforeTransitions: null, 22 | }); 23 | 24 | /** 25 | * Merge route into the global application state 26 | */ 27 | function routeReducer(state = routeInitialState, action) { 28 | switch (action.type) { 29 | /* istanbul ignore next */ 30 | case LOCATION_CHANGE: 31 | return state.merge({ 32 | locationBeforeTransitions: action.payload, 33 | }); 34 | default: 35 | return state; 36 | } 37 | } 38 | 39 | /** 40 | * Creates the main reducer with the asynchronously loaded ones 41 | */ 42 | export default function createReducer(asyncReducers) { 43 | return combineReducers({ 44 | route: routeReducer, 45 | language: languageProviderReducer, 46 | ...asyncReducers, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /app/static/conversation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litdevelopers/tinder/6eb771d682ef7f5ea537105556f3733ee1019502/app/static/conversation.png -------------------------------------------------------------------------------- /app/static/tinder_female.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litdevelopers/tinder/6eb771d682ef7f5ea537105556f3733ee1019502/app/static/tinder_female.png -------------------------------------------------------------------------------- /app/static/tinder_male.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litdevelopers/tinder/6eb771d682ef7f5ea537105556f3733ee1019502/app/static/tinder_male.png -------------------------------------------------------------------------------- /app/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the store with asynchronously loaded reducers 3 | */ 4 | 5 | import { createStore, applyMiddleware, compose } from 'redux'; 6 | import { fromJS } from 'immutable'; 7 | import { routerMiddleware } from 'react-router-redux'; 8 | import createSagaMiddleware from 'redux-saga'; 9 | import createReducer from './reducers'; 10 | 11 | import actionLoggerMiddleware from './utils/logger'; 12 | 13 | const sagaMiddleware = createSagaMiddleware(); 14 | const devtools = window.devToolsExtension || (() => noop => noop); 15 | 16 | export default function configureStore(initialState = {}, history) { 17 | // Create the store with two middlewares 18 | // 1. sagaMiddleware: Makes redux-sagas work 19 | // 2. routerMiddleware: Syncs the location/URL path to the state 20 | const middlewares = [ 21 | actionLoggerMiddleware, 22 | sagaMiddleware, 23 | routerMiddleware(history), 24 | ]; 25 | 26 | const enhancers = [ 27 | applyMiddleware(...middlewares), 28 | devtools(), 29 | ]; 30 | 31 | const store = createStore( 32 | createReducer(), 33 | fromJS(initialState), 34 | compose(...enhancers) 35 | ); 36 | 37 | // Create hook for async sagas 38 | store.runSaga = sagaMiddleware.run; 39 | 40 | // Make reducers hot reloadable, see http://mxs.is/googmo 41 | /* istanbul ignore next */ 42 | if (module.hot) { 43 | System.import('./reducers').then((reducerModule) => { 44 | const createReducers = reducerModule.default; 45 | const nextReducers = createReducers(store.asyncReducers); 46 | 47 | store.replaceReducer(nextReducers); 48 | }); 49 | } 50 | 51 | // Initialize it with no other reducers 52 | store.asyncReducers = {}; 53 | return store; 54 | } 55 | -------------------------------------------------------------------------------- /app/store.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test store addons 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from './store'; 7 | import { browserHistory } from 'react-router'; 8 | 9 | describe('configureStore', () => { 10 | let store; 11 | 12 | before(() => { 13 | store = configureStore({}, browserHistory); 14 | }); 15 | 16 | describe('asyncReducers', () => { 17 | it('should contain an object for async reducers', () => { 18 | expect(typeof store.asyncReducers).toEqual('object'); 19 | }); 20 | }); 21 | 22 | describe('runSaga', () => { 23 | it('should contain a hook for `sagaMiddleware.run`', () => { 24 | expect(typeof store.runSaga).toEqual('function'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /app/tests/store.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test store addons 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from '../store'; 7 | import { browserHistory } from 'react-router'; 8 | 9 | describe('configureStore', () => { 10 | let store; 11 | 12 | before(() => { 13 | store = configureStore({}, browserHistory); 14 | }); 15 | 16 | describe('asyncReducers', () => { 17 | it('should contain an object for async reducers', () => { 18 | expect(typeof store.asyncReducers).toEqual('object'); 19 | }); 20 | }); 21 | 22 | describe('runSaga', () => { 23 | it('should contain a hook for `sagaMiddleware.run`', () => { 24 | expect(typeof store.runSaga).toEqual('function'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /app/translations/en.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /app/utils/asyncInjectors.js: -------------------------------------------------------------------------------- 1 | import { conformsTo, isEmpty, isFunction, isObject, isString } from 'lodash'; 2 | import invariant from 'invariant'; 3 | import warning from 'warning'; 4 | import createReducer from 'reducers'; 5 | 6 | /** 7 | * Validate the shape of redux store 8 | */ 9 | export function checkStore(store) { 10 | const shape = { 11 | dispatch: isFunction, 12 | subscribe: isFunction, 13 | getState: isFunction, 14 | replaceReducer: isFunction, 15 | runSaga: isFunction, 16 | asyncReducers: isObject, 17 | }; 18 | invariant( 19 | conformsTo(store, shape), 20 | '(app/utils...) asyncInjectors: Expected a valid redux store' 21 | ); 22 | } 23 | 24 | /** 25 | * Inject an asynchronously loaded reducer 26 | */ 27 | export function injectAsyncReducer(store, isValid) { 28 | return function injectReducer(name, asyncReducer) { 29 | if (!isValid) checkStore(store); 30 | 31 | invariant( 32 | isString(name) && !isEmpty(name) && isFunction(asyncReducer), 33 | '(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function' 34 | ); 35 | 36 | store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign 37 | store.replaceReducer(createReducer(store.asyncReducers)); 38 | }; 39 | } 40 | 41 | /** 42 | * Inject an asynchronously loaded saga 43 | */ 44 | export function injectAsyncSagas(store, isValid) { 45 | return function injectSagas(sagas) { 46 | if (!isValid) checkStore(store); 47 | 48 | invariant( 49 | Array.isArray(sagas), 50 | '(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions' 51 | ); 52 | 53 | warning( 54 | !isEmpty(sagas), 55 | '(app/utils...) injectAsyncSagas: Received an empty `sagas` array' 56 | ); 57 | 58 | sagas.map(store.runSaga); 59 | }; 60 | } 61 | 62 | /** 63 | * Helper for creating injectors 64 | */ 65 | export function getAsyncInjectors(store) { 66 | checkStore(store); 67 | 68 | return { 69 | injectReducer: injectAsyncReducer(store, true), 70 | injectSagas: injectAsyncSagas(store, true), 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /app/utils/facebook.js: -------------------------------------------------------------------------------- 1 | export const getFacebookPicture = (id) => `https://graph.facebook.com/${id}/picture?type=large`; 2 | export const getFacebookUrl = (id) => `https://facebook.com/${id}`; 3 | export const getFacebookLinks = (id) => new Promise((resolve) => { resolve({ picture: getFacebookPicture(id), url: getFacebookUrl(id) }); }); 4 | -------------------------------------------------------------------------------- /app/utils/history.js: -------------------------------------------------------------------------------- 1 | import { storeToken } from './operations'; 2 | 3 | export function parseHistoryInput(data, type) { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /app/utils/logger.js: -------------------------------------------------------------------------------- 1 | import { 2 | LIKE_PERSON, 3 | PASS_PERSON, 4 | SUPERLIKE_PERSON, 5 | NEW_MATCH, 6 | } from '../containers/Recommendations/constants'; 7 | 8 | import { shiftDataWithToken } from './storage'; 9 | 10 | const recordedActions = [ 11 | LIKE_PERSON, 12 | PASS_PERSON, 13 | SUPERLIKE_PERSON, 14 | NEW_MATCH, 15 | ]; 16 | 17 | const actionLoggerMiddleware = () => next => action => { 18 | if (recordedActions.indexOf(action.type) !== -1) { 19 | shiftDataWithToken(`actionsHistory_${localStorage.getItem('tinderUserID')}`, action); 20 | } 21 | return next(action); 22 | }; 23 | 24 | export default actionLoggerMiddleware; 25 | -------------------------------------------------------------------------------- /app/utils/notifications.js: -------------------------------------------------------------------------------- 1 | export function createNotification(body, icon, title) { 2 | if (title.match(/match/) && window.location.pathname === '/dashboard/recommendations') return; 3 | const n = new Notification(title, { body, icon }); 4 | setTimeout(n.close.bind(n), 4000); 5 | } 6 | 7 | export function requestNotificationPermissions() { 8 | return new Promise((resolve, reject) => { 9 | Notification.requestPermission() 10 | .then((permissions) => resolve(permissions === 'granted')) 11 | .catch((err) => { 12 | reject(err); 13 | }); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /app/utils/operations.js: -------------------------------------------------------------------------------- 1 | function getCurrentYear() { 2 | return new Date().getFullYear(); 3 | } 4 | 5 | // Subtract one because I think I've seen so many incorrect ages by one year too many 6 | export function getAge(dateString) { 7 | return getCurrentYear() - new Date(dateString).getFullYear() - 1; 8 | } 9 | 10 | export function parsePingTime(dateString, text = true) { 11 | if (!dateString) return 'Active some time ago'; 12 | const currentDate = new Date().getTime(); 13 | const pingTimeDifferenceMinutes = (currentDate - new Date(dateString).getTime()) / 60000; 14 | 15 | if (pingTimeDifferenceMinutes < 60) { 16 | const minutes = pingTimeDifferenceMinutes.toFixed(0); 17 | if (Number(minutes) === 0) { 18 | return 'Just now'; 19 | } 20 | return `${text ? 'active' : ''} ${minutes} minute${Number(minutes) === 1 ? '' : 's'} ago`; 21 | } else if (pingTimeDifferenceMinutes / 60 < 24) { 22 | const hours = (pingTimeDifferenceMinutes / 60).toFixed(0); 23 | return `${text ? 'active' : ''} ${hours} hour${Number(hours) === 1 ? '' : 's'} ago`; 24 | } 25 | const days = (pingTimeDifferenceMinutes / 1440).toFixed(0); 26 | return `${text ? 'active' : ''} ${days} day${Number(days) === 1 ? '' : 's'} ago`; 27 | } 28 | 29 | export const convertDistanceToLocal = (distance) => Math.floor(distance * 1.6); 30 | 31 | export function mergeArray(arr1, arr2, length, mapFunc) { 32 | const allElements = [...arr1, ...arr2]; 33 | const filteredElements = allElements.map(mapFunc); 34 | if (length) return filteredElements.splice(0, length); 35 | return filteredElements; 36 | } 37 | 38 | export function matchesSortByDistance(a, b) { 39 | return a.distance_mi - b.distance_mi; 40 | } 41 | 42 | export function matchesSortByYoungest(a, b) { 43 | return getAge(a.birth_date) - getAge(b.birth_date); 44 | } 45 | 46 | export function matchesSortByOldest(a, b) { 47 | return getAge(b.birth_date) - getAge(a.birth_date); 48 | } 49 | 50 | export function messagesSortByRecent(a, b) { 51 | return new Date(b.last_activity_date).getTime() - new Date(a.last_activity_date).getTime(); 52 | } 53 | -------------------------------------------------------------------------------- /app/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export function postRequest(url, body) { 4 | return new Promise((resolve, reject) => { 5 | axios({ 6 | url, 7 | method: 'POST', 8 | data: body, 9 | timeout: 20000, 10 | }) 11 | .then((result) => { 12 | resolve(result); 13 | }) 14 | .catch((error) => { 15 | reject(error); 16 | }); 17 | }); 18 | } 19 | 20 | export function getRequest(url, queryParams) { 21 | return new Promise((resolve, reject) => { 22 | axios({ 23 | url, 24 | method: 'GET', 25 | params: queryParams, 26 | }) 27 | .then((result) => { 28 | resolve(result); 29 | }) 30 | .catch((errors) => { 31 | reject(errors); 32 | }); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /app/utils/storage.js: -------------------------------------------------------------------------------- 1 | import localForage from 'localforage'; 2 | 3 | export function storeToken(key, token) { 4 | return new Promise((resolve, reject) => { 5 | localForage.setItem(key, token) 6 | .then(() => resolve('Token Stored')) 7 | .catch((err) => reject(err)); 8 | }); 9 | } 10 | 11 | export function getToken(key) { 12 | return new Promise((resolve, reject) => { 13 | localForage.getItem(key) 14 | .then((token) => resolve(token)) 15 | .catch((error) => reject(error)); 16 | }); 17 | } 18 | 19 | export function removeToken(key) { 20 | return new Promise((resolve, reject) => { 21 | localForage.removeItem(key) 22 | .then((token) => resolve(token)) 23 | .catch((error) => reject(error)); 24 | }); 25 | } 26 | 27 | export function storeChunkWithToken(arrayData) { 28 | return new Promise((resolve, reject) => { 29 | const arrayId = []; 30 | if (!arrayData) reject(new Error('Array Data Missing')); 31 | for (let iter = 0; iter < arrayData.length; iter++) { 32 | localForage.setItem(arrayData[iter]._id, (arrayData[iter])); // eslint-disable-line 33 | arrayId.push(arrayData[iter]._id); // eslint-disable-line 34 | } 35 | resolve(arrayId); 36 | }); 37 | } 38 | 39 | export function removeChunkWithArray(arrayData) { 40 | return new Promise((resolve, reject) => { 41 | const actionsArray = []; 42 | for (let iter = 0; iter < arrayData.length; iter++) { 43 | actionsArray.push(localForage.removeItem(arrayData[iter])); 44 | } 45 | Promise.all(actionsArray).then(() => { 46 | resolve('Done!'); 47 | }) 48 | .catch((err) => reject(err)); 49 | }); 50 | } 51 | 52 | export function fetchChunkData(list) { 53 | return new Promise((resolve) => { 54 | const actionsArray = []; 55 | for (let i = 0; i < list.length; i++) { 56 | actionsArray.push(localForage.getItem(list[i])); 57 | } 58 | Promise.all(actionsArray).then((data) => { 59 | resolve(data); 60 | }); 61 | }); 62 | } 63 | 64 | export function getStoreLength() { 65 | return new Promise((resolve, reject) => { 66 | localForage.length().then((length) => resolve(length)).catch((errors) => reject(errors)); 67 | }); 68 | } 69 | 70 | export function createStore(storeName) { 71 | return localForage.createInstance({ 72 | name: storeName, 73 | }); 74 | } 75 | 76 | export function clearStore() { 77 | return new Promise((resolve, reject) => { 78 | localForage.clear().then(() => resolve('done')).catch((err) => reject(err)); 79 | }); 80 | } 81 | 82 | export function shiftDataWithToken(token, data) { 83 | return new Promise((resolve, reject) => { 84 | getToken(token) 85 | .then((retrivedData) => { 86 | storeToken(token, [data, ...retrivedData.filter((each) => each.id !== data.id)]).then(() => resolve('done!')); 87 | }) 88 | .catch((err) => reject(err)); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /app/utils/tests/asyncInjectors.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test async injectors 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from 'store.js'; 7 | import { memoryHistory } from 'react-router'; 8 | import { put } from 'redux-saga/effects'; 9 | import { fromJS } from 'immutable'; 10 | 11 | import { 12 | injectAsyncReducer, 13 | injectAsyncSagas, 14 | getAsyncInjectors, 15 | } from 'utils/asyncInjectors'; 16 | 17 | // Fixtures 18 | 19 | const initialState = fromJS({ reduced: 'soon' }); 20 | 21 | const reducer = (state = initialState, action) => { 22 | switch (action.type) { 23 | case 'TEST': 24 | return state.set('reduced', action.payload); 25 | default: 26 | return state; 27 | } 28 | }; 29 | 30 | function* testSaga() { 31 | yield put({ type: 'TEST', payload: 'yup' }); 32 | } 33 | 34 | const sagas = [ 35 | testSaga, 36 | ]; 37 | 38 | describe('asyncInjectors', () => { 39 | let store; 40 | 41 | describe('getAsyncInjectors', () => { 42 | before(() => { 43 | store = configureStore({}, memoryHistory); 44 | }); 45 | 46 | it('given a store, should return all async injectors', () => { 47 | const { injectReducer, injectSagas } = getAsyncInjectors(store); 48 | 49 | injectReducer('test', reducer); 50 | injectSagas(sagas); 51 | 52 | const actual = store.getState().get('test'); 53 | const expected = initialState.merge({ reduced: 'yup' }); 54 | 55 | expect(actual.toJS()).toEqual(expected.toJS()); 56 | }); 57 | 58 | it('should throw if passed invalid store shape', () => { 59 | let result = false; 60 | 61 | Reflect.deleteProperty(store, 'dispatch'); 62 | 63 | try { 64 | getAsyncInjectors(store); 65 | } catch (err) { 66 | result = err.name === 'Invariant Violation'; 67 | } 68 | 69 | expect(result).toEqual(true); 70 | }); 71 | }); 72 | 73 | describe('helpers', () => { 74 | before(() => { 75 | store = configureStore({}, memoryHistory); 76 | }); 77 | 78 | describe('injectAsyncReducer', () => { 79 | it('given a store, it should provide a function to inject a reducer', () => { 80 | const injectReducer = injectAsyncReducer(store); 81 | 82 | injectReducer('test', reducer); 83 | 84 | const actual = store.getState().get('test'); 85 | const expected = initialState; 86 | 87 | expect(actual.toJS()).toEqual(expected.toJS()); 88 | }); 89 | 90 | it('should throw if passed invalid name', () => { 91 | let result = false; 92 | 93 | const injectReducer = injectAsyncReducer(store); 94 | 95 | try { 96 | injectReducer('', reducer); 97 | } catch (err) { 98 | result = err.name === 'Invariant Violation'; 99 | } 100 | 101 | try { 102 | injectReducer(999, reducer); 103 | } catch (err) { 104 | result = err.name === 'Invariant Violation'; 105 | } 106 | 107 | expect(result).toEqual(true); 108 | }); 109 | 110 | it('should throw if passed invalid reducer', () => { 111 | let result = false; 112 | 113 | const injectReducer = injectAsyncReducer(store); 114 | 115 | try { 116 | injectReducer('bad', 'nope'); 117 | } catch (err) { 118 | result = err.name === 'Invariant Violation'; 119 | } 120 | 121 | try { 122 | injectReducer('coolio', 12345); 123 | } catch (err) { 124 | result = err.name === 'Invariant Violation'; 125 | } 126 | 127 | expect(result).toEqual(true); 128 | }); 129 | }); 130 | 131 | describe('injectAsyncSagas', () => { 132 | it('given a store, it should provide a function to inject a saga', () => { 133 | const injectSagas = injectAsyncSagas(store); 134 | 135 | injectSagas(sagas); 136 | 137 | const actual = store.getState().get('test'); 138 | const expected = initialState.merge({ reduced: 'yup' }); 139 | 140 | expect(actual.toJS()).toEqual(expected.toJS()); 141 | }); 142 | 143 | it('should throw if passed invalid saga', () => { 144 | let result = false; 145 | 146 | const injectSagas = injectAsyncSagas(store); 147 | 148 | try { 149 | injectSagas({ testSaga }); 150 | } catch (err) { 151 | result = err.name === 'Invariant Violation'; 152 | } 153 | 154 | try { 155 | injectSagas(testSaga); 156 | } catch (err) { 157 | result = err.name === 'Invariant Violation'; 158 | } 159 | 160 | expect(result).toEqual(true); 161 | }); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # http://www.appveyor.com/docs/appveyor-yml 2 | 3 | # Set build version format here instead of in the admin panel 4 | version: "{build}" 5 | 6 | # Do not build on gh tags 7 | skip_tags: true 8 | 9 | # Test against these versions of Node.js 10 | environment: 11 | 12 | matrix: 13 | # Node versions to run 14 | - nodejs_version: "5.0" 15 | 16 | # Fix line endings in Windows. (runs before repo cloning) 17 | init: 18 | - git config --global core.autocrlf input 19 | 20 | # Install scripts--runs after repo cloning 21 | install: 22 | # Install chrome 23 | - choco install -y googlechrome 24 | # Install the latest stable version of Node 25 | - ps: Install-Product node $env:nodejs_version 26 | - npm -g install npm 27 | - set PATH=%APPDATA%\npm;%PATH% 28 | - npm install 29 | 30 | # Disable automatic builds 31 | build: off 32 | 33 | # Post-install test scripts 34 | test_script: 35 | # Output debugging info 36 | - node --version 37 | - npm --version 38 | # run build and run tests 39 | - npm run build 40 | 41 | # remove, as appveyor doesn't support secure variables on pr builds 42 | # so `COVERALLS_REPO_TOKEN` cannot be set, without hard-coding in this file 43 | #on_success: 44 | #- npm run coveralls 45 | -------------------------------------------------------------------------------- /chrome/chrome.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litdevelopers/tinder/6eb771d682ef7f5ea537105556f3733ee1019502/chrome/chrome.crx -------------------------------------------------------------------------------- /chrome/src/ext/FBT.js: -------------------------------------------------------------------------------- 1 | // A Facebook token is needed to generate a proper key to authenticate, the user is first 2 | // Redirected to Facebook to approve the use of the app, and if the user approves, then it returns the token 3 | jQuery(function () // eslint-disable-line 4 | { 5 | console.log('ajaxing'); 6 | jQuery.ajax( // eslint-disable-line 7 | { 8 | url: 'https://www.facebook.com/v2.0/dialog/oauth/confirm', 9 | type: 'POST', 10 | data: { 11 | app_id: '464891386855067', 12 | fb_dtsg: $('input[name="fb_dtsg"]').val(), // eslint-disable-line 13 | ttstamp: '2658170904850115701205011500', 14 | redirect_uri: 'fbconnect://success', 15 | return_format: 'access_token', 16 | from_post: 1, 17 | display: 'popup', 18 | gdp_version: 4, 19 | sheet_name: 'initial', 20 | __CONFIRM__: 1, 21 | sso_device: '', 22 | ref: 'Default', 23 | }, 24 | success(html) { 25 | const token = html.match(/access_token=([\w_]+)&/i); 26 | // Just send the callback token to the page API 27 | window.location.href = 'http://localhost:3000/login/' + token[1]; // eslint-disable-line 28 | }, 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /chrome/src/ext/Facebook.js: -------------------------------------------------------------------------------- 1 | Lit.Facebook = (function (Lit) 2 | { 3 | //This redirects the current tab to a Facebook authorization page, nothing is done until the user authorizes the app 4 | function openAuthTab(tab) 5 | { 6 | chrome.tabs.update(tab, 7 | { 8 | url: 'https://www.facebook.com/v2.0/dialog/oauth?response_type=token&display=popup&api_key=464891386855067&redirect_uri=fbconnect%3A%2F%2Fsuccess&scope=user_about_me%2Cuser_activities%2Cuser_education_history%2Cuser_location%2Cuser_photos%2Cuser_relationship_details%2Cuser_status' 9 | }); 10 | } 11 | 12 | function chromeListener() 13 | { 14 | chrome.runtime.onMessageExternal.addListener(function (request, sender, sendResponse) 15 | { 16 | if (request.type === 'doAuth') 17 | { 18 | openAuthTab(sender.tab.id); 19 | } 20 | else if (request.message == "exists") 21 | { 22 | sendResponse( 23 | { 24 | version: 1.1 25 | }); 26 | } 27 | }); 28 | } 29 | 30 | return{ 31 | openAuthTab: openAuthTab, 32 | 33 | init: function () 34 | { 35 | chromeListener(); 36 | } 37 | }; 38 | })(Lit); -------------------------------------------------------------------------------- /chrome/src/ext/Lit.js: -------------------------------------------------------------------------------- 1 | var Lit = (function () 2 | { 3 | //The whole purpose of this extension is to get a token to enable authenthication and to provide a quick access to Lit 4 | var url = 'http://localhost:3000/login'; 5 | 6 | function newTab(tab) 7 | { 8 | if (tab) 9 | { 10 | chrome.tabs.update(tab, 11 | { 12 | url: url 13 | }); 14 | } 15 | else 16 | { 17 | chrome.tabs.create( 18 | { 19 | url: url 20 | }); 21 | } 22 | } 23 | 24 | function chromeListener() 25 | { 26 | chrome.browserAction.onClicked.addListener(function () 27 | { 28 | newTab(); 29 | }); 30 | } 31 | 32 | return{ 33 | newTab: newTab, 34 | 35 | init: function () 36 | { 37 | Lit.Facebook.init(); 38 | chromeListener(); 39 | } 40 | }; 41 | })(); -------------------------------------------------------------------------------- /chrome/src/ext/background.js: -------------------------------------------------------------------------------- 1 | Lit.init(); 2 | -------------------------------------------------------------------------------- /chrome/src/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litdevelopers/tinder/6eb771d682ef7f5ea537105556f3733ee1019502/chrome/src/icons/128x128.png -------------------------------------------------------------------------------- /chrome/src/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litdevelopers/tinder/6eb771d682ef7f5ea537105556f3733ee1019502/chrome/src/icons/16x16.png -------------------------------------------------------------------------------- /chrome/src/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litdevelopers/tinder/6eb771d682ef7f5ea537105556f3733ee1019502/chrome/src/icons/48x48.png -------------------------------------------------------------------------------- /chrome/src/icons/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litdevelopers/tinder/6eb771d682ef7f5ea537105556f3733ee1019502/chrome/src/icons/main.png -------------------------------------------------------------------------------- /chrome/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "scripts": [ "vendor/jquery.min.js", "ext/Lit.js", "ext/Facebook.js", "ext/background.js" ] 4 | }, 5 | "browser_action": { 6 | "default_icon": "icons/main.png", 7 | "default_title": "Lit" 8 | }, 9 | "content_scripts": [ { 10 | "include_globs": [ "*464891386855067*redirect_uri=fbconnect%3A%2F%2Fsuccess*", "*redirect_uri=fbconnect%3A%2F%2Fsuccess*464891386855067*" ], 11 | "js": [ "vendor/jquery.min.js", "ext/FBT.js" ], 12 | "matches": [ "https://www.facebook.com/v2.0/dialog/oauth*" ], 13 | "run_at": "document_start" 14 | } ], 15 | "description": "Tinder Web Client", 16 | "externally_connectable": { 17 | "matches": [ "http://localhost:3000/*" ] 18 | }, 19 | "icons": { 20 | "128": "icons/128x128.png", 21 | "16": "icons/16x16.png", 22 | "48": "icons/48x48.png" 23 | }, 24 | "manifest_version": 2, 25 | "name": "Lit", 26 | "permissions": [ "webRequest", "http://localhost:3000/*" ], 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /internals/config.js: -------------------------------------------------------------------------------- 1 | const resolve = require('path').resolve; 2 | const pullAll = require('lodash/pullAll'); 3 | const uniq = require('lodash/uniq'); 4 | 5 | const ReactBoilerplate = { 6 | // This refers to the react-boilerplate version this project is based on. 7 | version: '3.0.0', 8 | 9 | /** 10 | * The DLL Plugin provides a dramatic speed increase to webpack build and hot module reloading 11 | * by caching the module metadata for all of our npm dependencies. We enable it by default 12 | * in development. 13 | * 14 | * 15 | * To disable the DLL Plugin, set this value to false. 16 | */ 17 | dllPlugin: { 18 | defaults: { 19 | /** 20 | * we need to exclude dependencies which are not intended for the browser 21 | * by listing them here.container 22 | */ 23 | exclude: [ 24 | 'chalk', 25 | 'compression', 26 | 'cross-env', 27 | 'express', 28 | 'ip', 29 | 'minimist', 30 | 'sanitize.css' 31 | ], 32 | 33 | /** 34 | * Specify any additional dependencies here. We include core-js and lodash 35 | * since a lot of our dependencies depend on them and they get picked up by webpack. 36 | */ 37 | include: ['core-js', 'eventsource-polyfill', 'babel-polyfill', 'lodash'], 38 | 39 | // The path where the DLL manifest and bundle will get built 40 | path: resolve('../node_modules/react-boilerplate-dlls'), 41 | }, 42 | 43 | entry(pkg) { 44 | const dependencyNames = Object.keys(pkg.dependencies); 45 | const exclude = pkg.dllPlugin.exclude || ReactBoilerplate.dllPlugin.defaults.exclude; 46 | const include = pkg.dllPlugin.include || ReactBoilerplate.dllPlugin.defaults.include; 47 | const includeDependencies = uniq(dependencyNames.concat(include)); 48 | 49 | return { 50 | reactBoilerplateDeps: pullAll(includeDependencies, exclude), 51 | }; 52 | }, 53 | }, 54 | }; 55 | 56 | module.exports = ReactBoilerplate; 57 | -------------------------------------------------------------------------------- /internals/generators/component/es6.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | {{#if wantMessages}} 10 | import { FormattedMessage } from 'react-intl'; 11 | import messages from './messages'; 12 | {{/if}} 13 | {{#if wantCSS}} 14 | import styles from './styles.css'; 15 | {{/if}} 16 | 17 | class {{ properCase name }} extends React.Component { // eslint-disable-line react/prefer-stateless-function 18 | render() { 19 | return ( 20 | {{#if wantCSS}} 21 |
    22 | {{else}} 23 |
    24 | {{/if}} 25 | {{#if wantMessages}} 26 | 27 | {{/if}} 28 |
    29 | ); 30 | } 31 | } 32 | 33 | export default {{ properCase name }}; 34 | -------------------------------------------------------------------------------- /internals/generators/component/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Generator 3 | */ 4 | 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | module.exports = { 8 | description: 'Add an unconnected component', 9 | prompts: [{ 10 | type: 'list', 11 | name: 'type', 12 | message: 'Select the type of component', 13 | default: 'Stateless Function', 14 | choices: () => ['ES6 Class', 'Stateless Function'], 15 | }, { 16 | type: 'input', 17 | name: 'name', 18 | message: 'What should it be called?', 19 | default: 'Button', 20 | validate: value => { 21 | if ((/.+/).test(value)) { 22 | return componentExists(value) ? 'A component or container with this name already exists' : true; 23 | } 24 | 25 | return 'The name is required'; 26 | }, 27 | }, { 28 | type: 'confirm', 29 | name: 'wantCSS', 30 | default: true, 31 | message: 'Does it have styling?', 32 | }, { 33 | type: 'confirm', 34 | name: 'wantMessages', 35 | default: true, 36 | message: 'Do you want i18n messages (i.e. will this component use text)?', 37 | }], 38 | actions: data => { 39 | // Generate index.js and index.test.js 40 | const actions = [{ 41 | type: 'add', 42 | path: '../../app/components/{{properCase name}}/index.js', 43 | templateFile: data.type === 'ES6 Class' ? './component/es6.js.hbs' : './component/stateless.js.hbs', 44 | abortOnFail: true, 45 | }, { 46 | type: 'add', 47 | path: '../../app/components/{{properCase name}}/tests/index.test.js', 48 | templateFile: './component/test.js.hbs', 49 | abortOnFail: true, 50 | }]; 51 | 52 | // If they want a CSS file, add styles.css 53 | if (data.wantCSS) { 54 | actions.push({ 55 | type: 'add', 56 | path: '../../app/components/{{properCase name}}/styles.css', 57 | templateFile: './component/styles.css.hbs', 58 | abortOnFail: true, 59 | }); 60 | } 61 | 62 | // If they want a i18n messages file 63 | if (data.wantMessages) { 64 | actions.push({ 65 | type: 'add', 66 | path: '../../app/components/{{properCase name}}/messages.js', 67 | templateFile: './component/messages.js.hbs', 68 | abortOnFail: true, 69 | }); 70 | } 71 | 72 | return actions; 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /internals/generators/component/messages.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * {{ properCase name }} Messages 3 | * 4 | * This contains all the text for the {{ properCase name }} component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.{{ properCase name }}.header', 11 | defaultMessage: 'This is the {{ properCase name}} component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /internals/generators/component/stateless.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | {{#if wantMessages}} 10 | import { FormattedMessage } from 'react-intl'; 11 | import messages from './messages'; 12 | {{/if}} 13 | 14 | {{#if wantCSS}} 15 | import styles from './styles.css'; 16 | {{/if}} 17 | 18 | function {{ properCase name }}() { 19 | return ( 20 | {{#if wantCSS}} 21 |
    22 | {{else}} 23 |
    24 | {{/if}} 25 | {{#if wantMessages}} 26 | 27 | {{/if}} 28 |
    29 | ); 30 | } 31 | 32 | export default {{ properCase name }}; 33 | -------------------------------------------------------------------------------- /internals/generators/component/styles.css.hbs: -------------------------------------------------------------------------------- 1 | .{{ camelCase name }} { /* stylelint-ignore */ 2 | 3 | } 4 | -------------------------------------------------------------------------------- /internals/generators/component/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import {{ properCase name }} from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /internals/generators/container/actions.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} actions 4 | * 5 | */ 6 | 7 | import { 8 | DEFAULT_ACTION, 9 | } from './constants'; 10 | 11 | export function defaultAction() { 12 | return { 13 | type: DEFAULT_ACTION, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /internals/generators/container/actions.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | defaultAction, 4 | } from '../actions'; 5 | import { 6 | DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('{{ properCase name }} actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: DEFAULT_ACTION, 14 | }; 15 | expect(defaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /internals/generators/container/constants.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/{{ properCase name }}/DEFAULT_ACTION'; 8 | -------------------------------------------------------------------------------- /internals/generators/container/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Container Generator 3 | */ 4 | 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | module.exports = { 8 | description: 'Add a container component', 9 | prompts: [{ 10 | type: 'input', 11 | name: 'name', 12 | message: 'What should it be called?', 13 | default: 'Form', 14 | validate: value => { 15 | if ((/.+/).test(value)) { 16 | return componentExists(value) ? 'A component or container with this name already exists' : true; 17 | } 18 | 19 | return 'The name is required'; 20 | }, 21 | }, { 22 | type: 'confirm', 23 | name: 'wantHeaders', 24 | default: false, 25 | message: 'Do you want headers?', 26 | }, { 27 | type: 'confirm', 28 | name: 'wantCSS', 29 | default: false, 30 | message: 'Does it have styling?', 31 | }, { 32 | type: 'confirm', 33 | name: 'wantActionsAndReducer', 34 | default: true, 35 | message: 'Do you want an actions/constants/selectors/reducer tupel for this container?', 36 | }, { 37 | type: 'confirm', 38 | name: 'wantSagas', 39 | default: true, 40 | message: 'Do you want sagas for asynchronous flows? (e.g. fetching data)', 41 | }, { 42 | type: 'confirm', 43 | name: 'wantMessages', 44 | default: true, 45 | message: 'Do you want i18n messages (i.e. will this component use text)?', 46 | }], 47 | actions: data => { 48 | // Generate index.js and index.test.js 49 | const actions = [{ 50 | type: 'add', 51 | path: '../../app/containers/{{properCase name}}/index.js', 52 | templateFile: './container/index.js.hbs', 53 | abortOnFail: true, 54 | }, { 55 | type: 'add', 56 | path: '../../app/containers/{{properCase name}}/tests/index.test.js', 57 | templateFile: './container/test.js.hbs', 58 | abortOnFail: true, 59 | }]; 60 | 61 | // If they want a CSS file, add styles.css 62 | if (data.wantCSS) { 63 | actions.push({ 64 | type: 'add', 65 | path: '../../app/containers/{{properCase name}}/styles.css', 66 | templateFile: './container/styles.css.hbs', 67 | abortOnFail: true, 68 | }); 69 | } 70 | 71 | // If component wants messages 72 | if (data.wantMessages) { 73 | actions.push({ 74 | type: 'add', 75 | path: '../../app/containers/{{properCase name}}/messages.js', 76 | templateFile: './container/messages.js.hbs', 77 | abortOnFail: true, 78 | }); 79 | } 80 | 81 | // If they want actions and a reducer, generate actions.js, constants.js, 82 | // reducer.js and the corresponding tests for actions and the reducer 83 | if (data.wantActionsAndReducer) { 84 | // Actions 85 | actions.push({ 86 | type: 'add', 87 | path: '../../app/containers/{{properCase name}}/actions.js', 88 | templateFile: './container/actions.js.hbs', 89 | abortOnFail: true, 90 | }); 91 | actions.push({ 92 | type: 'add', 93 | path: '../../app/containers/{{properCase name}}/tests/actions.test.js', 94 | templateFile: './container/actions.test.js.hbs', 95 | abortOnFail: true, 96 | }); 97 | 98 | // Constants 99 | actions.push({ 100 | type: 'add', 101 | path: '../../app/containers/{{properCase name}}/constants.js', 102 | templateFile: './container/constants.js.hbs', 103 | abortOnFail: true, 104 | }); 105 | 106 | // Selectors 107 | actions.push({ 108 | type: 'add', 109 | path: '../../app/containers/{{properCase name}}/selectors.js', 110 | templateFile: './container/selectors.js.hbs', 111 | abortOnFail: true, 112 | }); 113 | actions.push({ 114 | type: 'add', 115 | path: '../../app/containers/{{properCase name}}/tests/selectors.test.js', 116 | templateFile: './container/selectors.test.js.hbs', 117 | abortOnFail: true, 118 | }); 119 | 120 | // Reducer 121 | actions.push({ 122 | type: 'add', 123 | path: '../../app/containers/{{properCase name}}/reducer.js', 124 | templateFile: './container/reducer.js.hbs', 125 | abortOnFail: true, 126 | }); 127 | actions.push({ 128 | type: 'add', 129 | path: '../../app/containers/{{properCase name}}/tests/reducer.test.js', 130 | templateFile: './container/reducer.test.js.hbs', 131 | abortOnFail: true, 132 | }); 133 | } 134 | 135 | // Sagas 136 | if (data.wantSagas) { 137 | actions.push({ 138 | type: 'add', 139 | path: '../../app/containers/{{properCase name}}/sagas.js', 140 | templateFile: './container/sagas.js.hbs', 141 | abortOnFail: true, 142 | }); 143 | actions.push({ 144 | type: 'add', 145 | path: '../../app/containers/{{properCase name}}/tests/sagas.test.js', 146 | templateFile: './container/sagas.test.js.hbs', 147 | abortOnFail: true, 148 | }); 149 | } 150 | 151 | return actions; 152 | }, 153 | }; 154 | -------------------------------------------------------------------------------- /internals/generators/container/index.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | {{#if wantHeaders}} 10 | import Helmet from 'react-helmet'; 11 | {{/if}} 12 | {{#if wantActionsAndReducer}} 13 | import select{{properCase name}} from './selectors'; 14 | {{/if}} 15 | {{#if wantMessages}} 16 | import { FormattedMessage } from 'react-intl'; 17 | import messages from './messages'; 18 | {{/if}} 19 | {{#if wantCSS}} 20 | import styles from './styles.css'; 21 | {{/if}} 22 | 23 | export class {{ properCase name }} extends React.Component { // eslint-disable-line react/prefer-stateless-function 24 | render() { 25 | return ( 26 | {{#if wantCSS}} 27 |
    28 | {{else}} 29 |
    30 | {{/if}} 31 | {{#if wantHeaders}} 32 | 38 | {{/if}} 39 | {{#if wantMessages}} 40 | 41 | {{/if}} 42 |
    43 | ); 44 | } 45 | } 46 | 47 | {{#if wantActionsAndReducer}} 48 | const mapStateToProps = select{{properCase name}}(); 49 | {{/if}} 50 | 51 | function mapDispatchToProps(dispatch) { 52 | return { 53 | dispatch, 54 | }; 55 | } 56 | 57 | {{#if wantActionsAndReducer}} 58 | export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }}); 59 | {{else}} 60 | export default connect(mapDispatchToProps)({{ properCase name }}); 61 | {{/if}} 62 | -------------------------------------------------------------------------------- /internals/generators/container/messages.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * {{properCase name }} Messages 3 | * 4 | * This contains all the text for the {{properCase name }} component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.{{properCase name }}.header', 11 | defaultMessage: 'This is {{properCase name}} container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /internals/generators/container/reducer.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | DEFAULT_ACTION, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({}); 13 | 14 | function {{ camelCase name }}Reducer(state = initialState, action) { 15 | switch (action.type) { 16 | case DEFAULT_ACTION: 17 | return state; 18 | default: 19 | return state; 20 | } 21 | } 22 | 23 | export default {{ camelCase name }}Reducer; 24 | -------------------------------------------------------------------------------- /internals/generators/container/reducer.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import {{ camelCase name }}Reducer from '../reducer'; 3 | import { fromJS } from 'immutable'; 4 | 5 | describe('{{ camelCase name }}Reducer', () => { 6 | it('returns the initial state', () => { 7 | expect({{ camelCase name }}Reducer(undefined, {})).toEqual(fromJS({})); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /internals/generators/container/sagas.js.hbs: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select } from 'redux-saga/effects'; 2 | 3 | // Individual exports for testing 4 | export function* defaultSaga() { 5 | return; 6 | } 7 | 8 | // All sagas to be loaded 9 | export default [ 10 | defaultSaga, 11 | ]; 12 | -------------------------------------------------------------------------------- /internals/generators/container/sagas.test.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | import expect from 'expect'; 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../sagas'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /internals/generators/container/selectors.js.hbs: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the {{ camelCase name }} state domain 5 | */ 6 | const select{{ properCase name }}Domain = () => state => state.get('{{ camelCase name }}'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | 13 | /** 14 | * Default selector used by {{ properCase name }} 15 | */ 16 | 17 | const select{{ properCase name }} = () => createSelector( 18 | select{{ properCase name }}Domain(), 19 | (substate) => substate.toJS() 20 | ); 21 | 22 | export default select{{ properCase name }}; 23 | export { 24 | select{{ properCase name }}Domain, 25 | }; 26 | -------------------------------------------------------------------------------- /internals/generators/container/selectors.test.js.hbs: -------------------------------------------------------------------------------- 1 | // import { select{{ properCase name }}Domain } from '../selectors'; 2 | // import { fromJS } from 'immutable'; 3 | import expect from 'expect'; 4 | 5 | // const selector = select{{ properCase name}}Domain(); 6 | 7 | describe('select{{ properCase name }}Domain', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect('Test case').toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /internals/generators/container/styles.css.hbs: -------------------------------------------------------------------------------- 1 | .{{ camelCase name }} { /* stylelint-disable */ 2 | 3 | } 4 | -------------------------------------------------------------------------------- /internals/generators/container/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import {{ properCase name }} from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /internals/generators/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * generator/index.js 3 | * 4 | * Exports the generators so plop knows them 5 | */ 6 | 7 | const fs = require('fs'); 8 | const componentGenerator = require('./component/index.js'); 9 | const containerGenerator = require('./container/index.js'); 10 | const routeGenerator = require('./route/index.js'); 11 | const languageGenerator = require('./language/index.js'); 12 | 13 | module.exports = (plop) => { 14 | plop.setGenerator('component', componentGenerator); 15 | plop.setGenerator('container', containerGenerator); 16 | plop.setGenerator('route', routeGenerator); 17 | plop.setGenerator('language', languageGenerator); 18 | plop.addHelper('directory', (comp) => { 19 | try { 20 | fs.accessSync(`app/containers/${comp}`, fs.F_OK); 21 | return `containers/${comp}`; 22 | } catch (e) { 23 | return `components/${comp}`; 24 | } 25 | }); 26 | plop.addHelper('curly', (object, open) => (open ? '{' : '}')); 27 | }; 28 | -------------------------------------------------------------------------------- /internals/generators/language/add-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1addLocaleData({{language}}LocaleData); 2 | -------------------------------------------------------------------------------- /internals/generators/language/app-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 2 | '{{language}}', 3 | -------------------------------------------------------------------------------- /internals/generators/language/format-translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1 {{language}}: formatTranslationMessages({{language}}TranslationMessages), 2 | -------------------------------------------------------------------------------- /internals/generators/language/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Language Generator 3 | */ 4 | const exec = require('child_process').exec; 5 | 6 | module.exports = { 7 | description: 'Add a langauge', 8 | prompts: [{ 9 | type: 'input', 10 | name: 'language', 11 | message: 'What is the language you want to add i18n support for (e.g. "fr", "de")?', 12 | default: 'fr', 13 | validate: value => { 14 | if ((/.+/).test(value) && value.length === 2) { 15 | return true; 16 | } 17 | 18 | return '2 character language specifier is required'; 19 | }, 20 | }], 21 | 22 | actions: () => { 23 | const actions = []; 24 | actions.push({ 25 | type: 'modify', 26 | path: '../../app/i18n.js', 27 | pattern: /('react-intl\/locale-data\/[a-z]+';\n)(?!.*'react-intl\/locale-data\/[a-z]+';)/g, 28 | templateFile: './language/intl-locale-data.hbs', 29 | }); 30 | actions.push({ 31 | type: 'modify', 32 | path: '../../app/i18n.js', 33 | pattern: /([\n\s'[a-z]+',)(?!.*[\n\s'[a-z]+',)/g, 34 | templateFile: './language/app-locale.hbs', 35 | }); 36 | actions.push({ 37 | type: 'modify', 38 | path: '../../app/i18n.js', 39 | pattern: /(from\s'.\/translations\/[a-z]+.json';\n)(?!.*from\s'.\/translations\/[a-z]+.json';)/g, 40 | templateFile: './language/translation-messages.hbs', 41 | }); 42 | actions.push({ 43 | type: 'modify', 44 | path: '../../app/i18n.js', 45 | pattern: /(addLocaleData\([a-z]+LocaleData\);\n)(?!.*addLocaleData\([a-z]+LocaleData\);)/g, 46 | templateFile: './language/add-locale-data.hbs', 47 | }); 48 | actions.push({ 49 | type: 'modify', 50 | path: '../../app/i18n.js', 51 | pattern: /([a-z]+:\sformatTranslationMessages\([a-z]+TranslationMessages\),\n)(?!.*[a-z]+:\sformatTranslationMessages\([a-z]+TranslationMessages\),)/g, 52 | templateFile: './language/format-translation-messages.hbs', 53 | }); 54 | actions.push({ 55 | type: 'add', 56 | path: '../../app/translations/{{language}}.json', 57 | templateFile: './language/translations-json.hbs', 58 | abortOnFail: true, 59 | }); 60 | actions.push({ 61 | type: 'modify', 62 | path: '../../app/app.js', 63 | pattern: /(System\.import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),\n)(?!.*System\.import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),)/g, 64 | templateFile: './language/polyfill-intl-locale.hbs', 65 | }); 66 | actions.push( 67 | () => { 68 | const cmd = 'npm run extract-intl'; 69 | exec(cmd, (err, result, stderr) => { 70 | if (err || stderr) { 71 | throw err || stderr; 72 | } 73 | process.stdout.write(result); 74 | }); 75 | } 76 | ); 77 | 78 | return actions; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /internals/generators/language/intl-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}LocaleData from 'react-intl/locale-data/{{language}}'; 2 | -------------------------------------------------------------------------------- /internals/generators/language/polyfill-intl-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 System.import('intl/locale-data/jsonp/{{language}}.js'), 2 | -------------------------------------------------------------------------------- /internals/generators/language/translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}TranslationMessages from './translations/{{language}}.json'; 2 | -------------------------------------------------------------------------------- /internals/generators/language/translations-json.hbs: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /internals/generators/route/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Route Generator 3 | */ 4 | const fs = require('fs'); 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | function reducerExists(comp) { 8 | try { 9 | fs.accessSync(`app/containers/${comp}/reducer.js`, fs.F_OK); 10 | return true; 11 | } catch (e) { 12 | return false; 13 | } 14 | } 15 | 16 | function sagasExists(comp) { 17 | try { 18 | fs.accessSync(`app/containers/${comp}/sagas.js`, fs.F_OK); 19 | return true; 20 | } catch (e) { 21 | return false; 22 | } 23 | } 24 | 25 | function trimTemplateFile(template) { 26 | // Loads the template file and trims the whitespace and then returns the content as a string. 27 | return fs.readFileSync(`internals/generators/route/${template}`, 'utf8').replace(/\s*$/, ''); 28 | } 29 | 30 | module.exports = { 31 | description: 'Add a route', 32 | prompts: [{ 33 | type: 'input', 34 | name: 'component', 35 | message: 'Which component should the route show?', 36 | validate: value => { 37 | if ((/.+/).test(value)) { 38 | return componentExists(value) ? true : `"${value}" doesn't exist.`; 39 | } 40 | 41 | return 'The path is required'; 42 | }, 43 | }, { 44 | type: 'input', 45 | name: 'path', 46 | message: 'Enter the path of the route.', 47 | default: '/about', 48 | validate: value => { 49 | if ((/.+/).test(value)) { 50 | return true; 51 | } 52 | 53 | return 'path is required'; 54 | }, 55 | }], 56 | 57 | // Add the route to the routes.js file above the error route 58 | // TODO smarter route adding 59 | actions: data => { 60 | const actions = []; 61 | if (reducerExists(data.component)) { 62 | data.useSagas = sagasExists(data.component); // eslint-disable-line no-param-reassign 63 | actions.push({ 64 | type: 'modify', 65 | path: '../../app/routes.js', 66 | pattern: /(\s{\n\s{0,}path: '\*',)/g, 67 | template: trimTemplateFile('routeWithReducer.hbs'), 68 | }); 69 | } else { 70 | actions.push({ 71 | type: 'modify', 72 | path: '../../app/routes.js', 73 | pattern: /(\s{\n\s{0,}path: '\*',)/g, 74 | template: trimTemplateFile('route.hbs'), 75 | }); 76 | } 77 | 78 | return actions; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /internals/generators/route/route.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | name: '{{ camelCase component }}', 4 | getComponent(location, cb) { 5 | System.import('{{{directory (properCase component)}}}') 6 | .then(loadModule(cb)) 7 | .catch(errorLoading); 8 | }, 9 | },$1 10 | -------------------------------------------------------------------------------- /internals/generators/route/routeWithReducer.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | name: '{{ camelCase component }}', 4 | getComponent(nextState, cb) { 5 | const importModules = Promise.all([ 6 | System.import('containers/{{ properCase component }}/reducer'), 7 | {{#if useSagas}} 8 | System.import('containers/{{ properCase component }}/sagas'), 9 | {{/if}} 10 | System.import('containers/{{ properCase component }}'), 11 | ]); 12 | 13 | const renderRoute = loadModule(cb); 14 | 15 | importModules.then(([reducer,{{#if useSagas}} sagas,{{/if}} component]) => { 16 | injectReducer('{{ camelCase component }}', reducer.default); 17 | {{#if useSagas}} 18 | injectSagas(sagas.default); 19 | {{/if}} 20 | renderRoute(component); 21 | }); 22 | 23 | importModules.catch(errorLoading); 24 | }, 25 | },$1 26 | -------------------------------------------------------------------------------- /internals/generators/utils/componentExists.js: -------------------------------------------------------------------------------- 1 | /** 2 | * componentExists 3 | * 4 | * Check whether the given component exist in either the components or containers directory 5 | */ 6 | 7 | const fs = require('fs'); 8 | const pageComponents = fs.readdirSync('app/components'); 9 | const pageContainers = fs.readdirSync('app/containers'); 10 | const components = pageComponents.concat(pageContainers); 11 | 12 | function componentExists(comp) { 13 | return components.indexOf(comp) >= 0; 14 | } 15 | 16 | module.exports = componentExists; 17 | -------------------------------------------------------------------------------- /internals/scripts/analyze.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var shelljs = require('shelljs'); 4 | var animateProgress = require('./helpers/progress'); 5 | var chalk = require('chalk'); 6 | var addCheckMark = require('./helpers/checkmark'); 7 | 8 | var progress = animateProgress('Generating stats'); 9 | 10 | // Generate stats.json file with webpack 11 | shelljs.exec( 12 | 'webpack --config internals/webpack/webpack.prod.babel.js --profile --json > stats.json', 13 | addCheckMark.bind(null, callback) // Output a checkmark on completion 14 | ); 15 | 16 | // Called after webpack has finished generating the stats.json file 17 | function callback() { 18 | clearInterval(progress); 19 | process.stdout.write( 20 | '\n\nOpen ' + chalk.magenta('http://webpack.github.io/analyse/') + ' in your browser and upload the stats.json file!' + 21 | chalk.blue('\n(Tip: ' + chalk.italic('CMD + double-click') + ' the link!)\n\n') 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /internals/scripts/clean.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | 3 | /** 4 | * Adds mark check symbol 5 | */ 6 | function addCheckMark(callback) { 7 | process.stdout.write(' ✓'); 8 | callback(); 9 | } 10 | 11 | if (!which('git')) { 12 | echo('Sorry, this script requires git'); 13 | exit(1); 14 | } 15 | 16 | if (!test('-e', 'internals/templates')) { 17 | echo('The example is deleted already.'); 18 | exit(1); 19 | } 20 | 21 | process.stdout.write('Cleanup started...'); 22 | 23 | // Cleanup components folder 24 | rm('-rf', 'app/components/*'); 25 | 26 | // Cleanup containers folder 27 | rm('-rf', 'app/containers/*'); 28 | mkdir('-p', 'app/containers/App'); 29 | mkdir('-p', 'app/containers/NotFoundPage'); 30 | mkdir('-p', 'app/containers/HomePage'); 31 | cp('internals/templates/appContainer.js', 'app/containers/App/index.js'); 32 | cp('internals/templates/notFoundPage/notFoundPage.js', 'app/containers/NotFoundPage/index.js'); 33 | cp('internals/templates/notFoundPage/messages.js', 'app/containers/NotFoundPage/messages.js'); 34 | cp('internals/templates/homePage/homePage.js', 'app/containers/HomePage/index.js'); 35 | cp('internals/templates/homePage/messages.js', 'app/containers/HomePage/messages.js'); 36 | 37 | // Handle Translations 38 | mkdir('-p', 'app/translations'); 39 | cp('internals/templates/translations/en.json', 40 | 'app/translations/en.json'); 41 | 42 | // move i18n file 43 | cp('internals/templates/i18n.js', 44 | 'app/i18n.js'); 45 | 46 | // Copy LanguageProvider 47 | mkdir('-p', 'app/containers/LanguageProvider'); 48 | mkdir('-p', 'app/containers/LanguageProvider/tests'); 49 | cp('internals/templates/languageProvider/actions.js', 50 | 'app/containers/LanguageProvider/actions.js'); 51 | cp('internals/templates/languageProvider/constants.js', 52 | 'app/containers/LanguageProvider/constants.js'); 53 | cp('internals/templates/languageProvider/languageProvider.js', 54 | 'app/containers/LanguageProvider/index.js'); 55 | cp('internals/templates/languageProvider/reducer.js', 56 | 'app/containers/LanguageProvider/reducer.js'); 57 | cp('internals/templates/languageProvider/selectors.js', 58 | 'app/containers/LanguageProvider/selectors.js'); 59 | cp('internals/templates/styles.css', 'app/containers/App/styles.css'); 60 | 61 | // Copy selectors 62 | mkdir('app/containers/App/tests'); 63 | cp('internals/templates/selectors.js', 64 | 'app/containers/App/selectors.js'); 65 | cp('internals/templates/selectors.test.js', 66 | 'app/containers/App/tests/selectors.test.js'); 67 | 68 | // Utils 69 | rm('-rf', 'app/utils'); 70 | mkdir('app/utils'); 71 | mkdir('app/utils/tests'); 72 | cp('internals/templates/asyncInjectors.js', 73 | 'app/utils/asyncInjectors.js'); 74 | cp('internals/templates/asyncInjectors.test.js', 75 | 'app/utils/tests/asyncInjectors.test.js'); 76 | 77 | // Replace the files in the root app/ folder 78 | cp('internals/templates/app.js', 'app/app.js'); 79 | cp('internals/templates/index.html', 'app/index.html'); 80 | cp('internals/templates/reducers.js', 'app/reducers.js'); 81 | cp('internals/templates/routes.js', 'app/routes.js'); 82 | cp('internals/templates/store.js', 'app/store.js'); 83 | cp('internals/templates/store.test.js', 'app/store.test.js'); 84 | 85 | // Remove the templates folder 86 | rm('-rf', 'internals/templates'); 87 | 88 | process.stdout.write(' ✓'); 89 | 90 | // Commit the changes 91 | if (exec('git add . --all && git commit -qm "Remove default example"').code !== 0) { 92 | echo('\nError: Git commit failed'); 93 | exit(1); 94 | } 95 | 96 | echo('\nCleanup done. Happy Coding!!!'); 97 | -------------------------------------------------------------------------------- /internals/scripts/dependencies.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | 3 | // No need to build the DLL in production 4 | if (process.env.NODE_ENV === 'production') { 5 | process.exit(0) 6 | } 7 | 8 | require('shelljs/global') 9 | 10 | const path = require('path') 11 | const fs = require('fs') 12 | const exists = fs.existsSync 13 | const writeFile = fs.writeFileSync 14 | 15 | const defaults = require('lodash/defaultsDeep') 16 | const pkg = require(path.join(process.cwd(), 'package.json')) 17 | const config = require('../config') 18 | const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults) 19 | const outputPath = path.join(process.cwd(), dllConfig.path) 20 | const dllManifestPath = path.join(outputPath, 'package.json') 21 | 22 | /** 23 | * I use node_modules/react-boilerplate-dlls by default just because 24 | * it isn't going to be version controlled and babel wont try to parse it. 25 | */ 26 | mkdir('-p', outputPath) 27 | 28 | echo('Building the Webpack DLL...') 29 | 30 | /** 31 | * Create a manifest so npm install doesnt warn us 32 | */ 33 | if (!exists(dllManifestPath)) { 34 | writeFile( 35 | dllManifestPath, 36 | JSON.stringify(defaults({ 37 | name: 'react-boilerplate-dlls', 38 | private: true, 39 | author: pkg.author, 40 | repository: pkg.repository, 41 | version: pkg.version 42 | }), null, 2), 43 | 44 | 'utf8' 45 | ) 46 | } 47 | 48 | // the BUILDING_DLL env var is set to avoid confusing the development environment 49 | exec('cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js') 50 | -------------------------------------------------------------------------------- /internals/scripts/helpers/checkmark.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | 3 | /** 4 | * Adds mark check symbol 5 | */ 6 | function addCheckMark(callback) { 7 | process.stdout.write(chalk.green(' ✓')); 8 | callback(); 9 | } 10 | 11 | module.exports = addCheckMark; 12 | -------------------------------------------------------------------------------- /internals/scripts/helpers/progress.js: -------------------------------------------------------------------------------- 1 | var readline = require('readline'); 2 | 3 | /** 4 | * Adds an animated progress indicator 5 | * 6 | * @param {string} message The message to write next to the indicator 7 | * @param {number} amountOfDots The amount of dots you want to animate 8 | */ 9 | function animateProgress(message, amountOfDots) { 10 | if (typeof amountOfDots !== 'number') { 11 | amountOfDots = 3; 12 | } 13 | 14 | var i = 0; 15 | return setInterval(function () { 16 | readline.cursorTo(process.stdout, 0); 17 | i = (i + 1) % (amountOfDots + 1); 18 | var dots = new Array(i + 1).join('.'); 19 | process.stdout.write(message + dots); 20 | }, 500); 21 | } 22 | 23 | module.exports = animateProgress; 24 | -------------------------------------------------------------------------------- /internals/scripts/npmcheckversion.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var exec = require('child_process').exec; 3 | exec('npm -v', function (err, stdout, stderr) { 4 | if (err) throw err; 5 | if (parseFloat(stdout) < 3) { 6 | throw new Error('[ERROR: React Boilerplate] You need npm version @>=3'); 7 | process.exit(1); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /internals/scripts/pagespeed.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.stdin.resume(); 4 | process.stdin.setEncoding('utf8'); 5 | 6 | var ngrok = require('ngrok'); 7 | var psi = require('psi'); 8 | var chalk = require('chalk'); 9 | 10 | log('\nStarting ngrok tunnel'); 11 | 12 | startTunnel(runPsi); 13 | 14 | function runPsi(url) { 15 | log('\nStarting PageSpeed Insights'); 16 | psi.output(url).then(function (err) { 17 | process.exit(0); 18 | }); 19 | } 20 | 21 | function startTunnel(cb) { 22 | ngrok.connect(3000, function (err, url) { 23 | if (err) { 24 | log(chalk.red('\nERROR\n' + err)); 25 | process.exit(0); 26 | } 27 | 28 | log('\nServing tunnel from: ' + chalk.magenta(url)); 29 | cb(url); 30 | }); 31 | } 32 | 33 | function log(string) { 34 | process.stdout.write(string); 35 | } 36 | -------------------------------------------------------------------------------- /internals/testing/karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('../webpack/webpack.test.babel'); 2 | const argv = require('minimist')(process.argv.slice(2)); 3 | const path = require('path'); 4 | 5 | module.exports = (config) => { 6 | config.set({ 7 | frameworks: ['mocha'], 8 | reporters: ['coverage', 'mocha'], 9 | browsers: process.env.TRAVIS // eslint-disable-line no-nested-ternary 10 | ? ['ChromeTravis'] 11 | : process.env.APPVEYOR 12 | ? ['IE'] : ['Chrome'], 13 | 14 | autoWatch: false, 15 | singleRun: true, 16 | 17 | client: { 18 | mocha: { 19 | grep: argv.grep, 20 | }, 21 | }, 22 | 23 | files: [ 24 | { 25 | pattern: './test-bundler.js', 26 | watched: false, 27 | served: true, 28 | included: true, 29 | }, 30 | ], 31 | 32 | preprocessors: { 33 | ['./test-bundler.js']: ['webpack', 'sourcemap'], // eslint-disable-line no-useless-computed-key 34 | }, 35 | 36 | webpack: webpackConfig, 37 | 38 | // make Webpack bundle generation quiet 39 | webpackMiddleware: { 40 | noInfo: true, 41 | stats: 'errors-only', 42 | }, 43 | 44 | customLaunchers: { 45 | ChromeTravis: { 46 | base: 'Chrome', 47 | flags: ['--no-sandbox'], 48 | }, 49 | }, 50 | 51 | coverageReporter: { 52 | dir: path.join(process.cwd(), 'coverage'), 53 | reporters: [ 54 | { type: 'lcov', subdir: 'lcov' }, 55 | { type: 'html', subdir: 'html' }, 56 | { type: 'text-summary' }, 57 | ], 58 | }, 59 | 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /internals/testing/test-bundler.js: -------------------------------------------------------------------------------- 1 | // needed for regenerator-runtime 2 | // (ES7 generator support is required by redux-saga) 3 | import 'babel-polyfill'; 4 | 5 | // If we need to use Chai, we'll have already chaiEnzyme loaded 6 | import chai from 'chai'; 7 | import chaiEnzyme from 'chai-enzyme'; 8 | chai.use(chaiEnzyme()); 9 | 10 | // Include all .js files under `app`, except app.js, reducers.js, routes.js and 11 | // store.js. This is for isparta code coverage 12 | const context = require.context('../../app', true, /^^((?!(app|reducers|routes|store)).)*\.js$/); 13 | context.keys().forEach(context); 14 | -------------------------------------------------------------------------------- /internals/webpack/webpack.base.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * COMMON WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | 8 | module.exports = (options) => ({ 9 | entry: options.entry, 10 | output: Object.assign({ // Compile into js/build.js 11 | path: path.resolve(process.cwd(), 'build'), 12 | publicPath: '/', 13 | }, options.output), // Merge with env dependent settings 14 | module: { 15 | loaders: [{ 16 | test: /\.js$/, // Transform all .js files required somewhere with Babel 17 | loader: 'babel', 18 | exclude: /node_modules/, 19 | query: options.babelQuery, 20 | }, { 21 | // Transform our own .css files with PostCSS and CSS-modules 22 | test: /\.css$/, 23 | exclude: /node_modules/, 24 | loader: options.cssLoaders, 25 | }, { 26 | // Do not transform vendor's CSS with CSS-modules 27 | // The point is that they remain in global scope. 28 | // Since we require these CSS files in our JS or CSS files, 29 | // they will be a part of our compilation either way. 30 | // So, no need for ExtractTextPlugin here. 31 | test: /\.css$/, 32 | include: /node_modules/, 33 | loaders: ['style-loader', 'css-loader'], 34 | }, { 35 | test: /\.(eot|svg|ttf|woff|woff2)$/, 36 | loader: 'file-loader', 37 | }, { 38 | test: /\.(jpg|png|gif)$/, 39 | loaders: [ 40 | 'file-loader', 41 | 'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}', 42 | ], 43 | }, { 44 | test: /\.html$/, 45 | loader: 'html-loader', 46 | }, { 47 | test: /\.json$/, 48 | loader: 'json-loader', 49 | }, { 50 | test: /\.(mp4|webm)$/, 51 | loader: 'url-loader?limit=10000', 52 | }], 53 | }, 54 | plugins: options.plugins.concat([ 55 | new webpack.ProvidePlugin({ 56 | // make fetch available 57 | fetch: 'exports?self.fetch!whatwg-fetch', 58 | }), 59 | 60 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` 61 | // inside your code for any environment checks; UglifyJS will automatically 62 | // drop any unreachable code. 63 | new webpack.DefinePlugin({ 64 | 'process.env': { 65 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 66 | }, 67 | }), 68 | ]), 69 | postcss: () => options.postcssPlugins, 70 | resolve: { 71 | modules: ['app', 'node_modules'], 72 | extensions: [ 73 | '', 74 | '.js', 75 | '.jsx', 76 | '.react.js', 77 | ], 78 | mainFields: [ 79 | 'jsnext:main', 80 | 'main', 81 | ], 82 | }, 83 | devtool: options.devtool, 84 | target: 'web', // Make web variables accessible to webpack, e.g. window 85 | stats: false, // Don't show stats in the console 86 | progress: true, 87 | }); 88 | -------------------------------------------------------------------------------- /internals/webpack/webpack.dll.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WEBPACK DLL GENERATOR 3 | * 4 | * This profile is used to cache webpack's module 5 | * contexts for external library and framework type 6 | * dependencies which will usually not change often enough 7 | * to warrant building them from scratch every time we use 8 | * the webpack process. 9 | */ 10 | 11 | const { join } = require('path'); 12 | const defaults = require('lodash/defaultsDeep'); 13 | const webpack = require('webpack'); 14 | const pkg = require(join(process.cwd(), 'package.json')); 15 | const dllPlugin = require('../config').dllPlugin; 16 | 17 | if (!pkg.dllPlugin) { process.exit(0); } 18 | 19 | const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults); 20 | const outputPath = join(process.cwd(), dllConfig.path); 21 | 22 | module.exports = { 23 | context: process.cwd(), 24 | entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg), 25 | devtool: 'eval', 26 | output: { 27 | filename: '[name].dll.js', 28 | path: outputPath, 29 | library: '[name]', 30 | }, 31 | plugins: [ 32 | new webpack.DllPlugin({ name: '[name]', path: join(outputPath, '[name].json') }), // eslint-disable-line no-new 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /internals/webpack/webpack.prod.babel.js: -------------------------------------------------------------------------------- 1 | // Important modules this config uses 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const OfflinePlugin = require('offline-plugin'); 7 | 8 | // PostCSS plugins 9 | const cssnext = require('postcss-cssnext'); 10 | const postcssFocus = require('postcss-focus'); 11 | const postcssReporter = require('postcss-reporter'); 12 | 13 | module.exports = require('./webpack.base.babel')({ 14 | // In production, we skip all hot-reloading stuff 15 | entry: [ 16 | path.join(process.cwd(), 'app/app.js'), 17 | ], 18 | 19 | // Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets 20 | output: { 21 | filename: '[name].[chunkhash].js', 22 | chunkFilename: '[name].[chunkhash].chunk.js', 23 | }, 24 | 25 | // We use ExtractTextPlugin so we get a seperate CSS file instead 26 | // of the CSS being in the JS and injected as a style tag 27 | cssLoaders: ExtractTextPlugin.extract( 28 | 'style-loader', 29 | 'css-loader?modules&-autoprefixer&importLoaders=1!postcss-loader' 30 | ), 31 | 32 | // In production, we minify our CSS with cssnano 33 | postcssPlugins: [ 34 | postcssFocus(), 35 | cssnext({ 36 | browsers: ['last 2 versions', 'IE > 10'], 37 | }), 38 | postcssReporter({ 39 | clearMessages: true, 40 | }), 41 | ], 42 | plugins: [ 43 | new webpack.optimize.CommonsChunkPlugin({ 44 | name: 'vendor', 45 | children: true, 46 | minChunks: 2, 47 | async: true, 48 | }), 49 | 50 | // OccurrenceOrderPlugin is needed for long-term caching to work properly. 51 | // See http://mxs.is/googmv 52 | new webpack.optimize.OccurrenceOrderPlugin(true), 53 | 54 | // Merge all duplicate modules 55 | new webpack.optimize.DedupePlugin(), 56 | 57 | // Minify and optimize the JavaScript 58 | new webpack.optimize.UglifyJsPlugin({ 59 | compress: { 60 | warnings: false, // ...but do not show warnings in the console (there is a lot of them) 61 | }, 62 | }), 63 | 64 | // Minify and optimize the index.html 65 | new HtmlWebpackPlugin({ 66 | template: 'app/index.html', 67 | minify: { 68 | removeComments: true, 69 | collapseWhitespace: true, 70 | removeRedundantAttributes: true, 71 | useShortDoctype: true, 72 | removeEmptyAttributes: true, 73 | removeStyleLinkTypeAttributes: true, 74 | keepClosingSlash: true, 75 | minifyJS: true, 76 | minifyCSS: true, 77 | minifyURLs: true, 78 | }, 79 | inject: true, 80 | }), 81 | 82 | // Extract the CSS into a seperate file 83 | new ExtractTextPlugin('[name].[contenthash].css'), 84 | 85 | // Put it in the end to capture all the HtmlWebpackPlugin's 86 | // assets manipulations and do leak its manipulations to HtmlWebpackPlugin 87 | new OfflinePlugin({ 88 | relativePaths: false, 89 | publicPath: '/', 90 | 91 | // No need to cache .htaccess. See http://mxs.is/googmp, 92 | // this is applied before any match in `caches` section 93 | excludes: ['.htaccess'], 94 | 95 | caches: { 96 | main: [':rest:'], 97 | 98 | // All chunks marked as `additional`, loaded after main section 99 | // and do not prevent SW to install. Change to `optional` if 100 | // do not want them to be preloaded at all (cached only when first loaded) 101 | additional: ['*.chunk.js'], 102 | }, 103 | 104 | // Removes warning for about `additional` section usage 105 | safeToUseOptionalCaches: true, 106 | 107 | AppCache: false, 108 | }), 109 | ], 110 | }); 111 | -------------------------------------------------------------------------------- /internals/webpack/webpack.test.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TEST WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const modules = [ 8 | 'app', 9 | 'node_modules', 10 | ]; 11 | 12 | module.exports = { 13 | devtool: 'inline-source-map', 14 | isparta: { 15 | babel: { 16 | presets: ['es2015', 'react', 'stage-0'], 17 | }, 18 | }, 19 | module: { 20 | // Some libraries don't like being run through babel. 21 | // If they gripe, put them here. 22 | noParse: [ 23 | /node_modules(\\|\/)sinon/, 24 | /node_modules(\\|\/)acorn/, 25 | ], 26 | preLoaders: [ 27 | { test: /\.js$/, 28 | loader: 'isparta', 29 | include: path.resolve('app/'), 30 | }, 31 | ], 32 | loaders: [ 33 | { test: /\.json$/, loader: 'json-loader' }, 34 | { test: /\.css$/, loader: 'null-loader' }, 35 | 36 | // sinon.js--aliased for enzyme--expects/requires global vars. 37 | // imports-loader allows for global vars to be injected into the module. 38 | // See https://github.com/webpack/webpack/issues/304 39 | { test: /sinon(\\|\/)pkg(\\|\/)sinon\.js/, 40 | loader: 'imports?define=>false,require=>false', 41 | }, 42 | { test: /\.js$/, 43 | loader: 'babel', 44 | exclude: [/node_modules/], 45 | }, 46 | { test: /\.jpe?g$|\.gif$|\.png$/i, 47 | loader: 'null-loader', 48 | }, 49 | ], 50 | }, 51 | 52 | plugins: [ 53 | 54 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` 55 | // inside your code for any environment checks; UglifyJS will automatically 56 | // drop any unreachable code. 57 | new webpack.DefinePlugin({ 58 | 'process.env': { 59 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 60 | }, 61 | })], 62 | 63 | // Some node_modules pull in Node-specific dependencies. 64 | // Since we're running in a browser we have to stub them out. See: 65 | // https://webpack.github.io/docs/configuration.html#node 66 | // https://github.com/webpack/node-libs-browser/tree/master/mock 67 | // https://github.com/webpack/jade-loader/issues/8#issuecomment-55568520 68 | node: { 69 | fs: 'empty', 70 | child_process: 'empty', 71 | net: 'empty', 72 | tls: 'empty', 73 | }, 74 | 75 | // required for enzyme to work properly 76 | externals: { 77 | jsdom: 'window', 78 | 'react/addons': true, 79 | 'react/lib/ExecutionEnvironment': true, 80 | 'react/lib/ReactContext': 'window', 81 | }, 82 | resolve: { 83 | modulesDirectories: modules, 84 | modules, 85 | alias: { 86 | // required for enzyme to work properly 87 | sinon: 'sinon/pkg/sinon', 88 | }, 89 | }, 90 | }; 91 | -------------------------------------------------------------------------------- /media/messages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litdevelopers/tinder/6eb771d682ef7f5ea537105556f3733ee1019502/media/messages.png -------------------------------------------------------------------------------- /media/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litdevelopers/tinder/6eb771d682ef7f5ea537105556f3733ee1019502/media/profile.png -------------------------------------------------------------------------------- /media/recommendations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litdevelopers/tinder/6eb771d682ef7f5ea537105556f3733ee1019502/media/recommendations.png -------------------------------------------------------------------------------- /server/api/facebookAuth.js: -------------------------------------------------------------------------------- 1 | const request = require('superagent'); 2 | 3 | function getFBToken(data) { 4 | return new Promise((resolve, reject) => { 5 | const tokenPattern = /access_token=(.*)&/; 6 | const token = data.match(tokenPattern)[1]; 7 | if (token) { 8 | resolve(token); 9 | } else { 10 | reject('Token error'); 11 | } 12 | // Keep code in case we want to start storing statically 13 | // const expirationPattern = /expires_in=(.*)/; 14 | // const expiration = parseInt(result.match(expirationPattern)[1]); 15 | // const now = Date.now(); 16 | // const expiryTime = new Date(now + (1000 * expiration)); 17 | }); 18 | } 19 | 20 | // Keep code in case in future we need fbid again 21 | function getFBUserId(token) { 22 | return new Promise((resolve, reject) => { 23 | const graphUrl = `https://graph.facebook.com/me?access_token=${token}`; 24 | request.get(graphUrl) 25 | .end((err, res) => { 26 | if (err || !res.ok) { 27 | reject('Superagent error'); 28 | } else { 29 | resolve(JSON.parse(res.text).id); 30 | } 31 | }); 32 | }); 33 | } 34 | 35 | function returnFacebook(data) { 36 | const token = getFBToken(data); 37 | const fbid = token.then((fbtoken) => getFBUserId(fbtoken)); 38 | return Promise.all([token, fbid]); 39 | } 40 | module.exports = { 41 | getFBToken, 42 | getFBUserId, 43 | returnFacebook, 44 | }; 45 | -------------------------------------------------------------------------------- /server/api/shelvedApi.txt: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | NEW AUTH FLOW BECAUSE I BROKE THE OLD ONE: 4 | 1) Go to Facebook.com. 5 | 2) If I can find an element for my profile picture, I know im in the right place. 6 | 2 - 1) If I can, I will go to the FACEBOOK_AUTH_URL 7 | 2 - 2) Done 8 | 3) If I cannot find my profile picture, I will go to facebook.com and login 9 | 3 - 1) I will then retrieve my profile id 10 | 3 - 2) I will THEN go on the website for oauth 11 | 12 | 13 | 14 | */ 15 | const FACEBOOK_AUTH_URL = 'https://www.facebook.com/dialog/oauth?client_id=464891386855067&redirect_uri=https://www.facebook.com/connect/login_success.html&scope=basic_info,email,public_profile,user_about_me,user_activities,user_birthday,user_education_history,user_friends,user_interests,user_likes,user_location,user_photos,user_relationship_details&response_type=token'; 16 | const NEW_FACEBOOK = 'https://www.facebook.com/v2.6/dialog/oauth?redirect_uri=fb464891386855067%3A%2F%2Fauthorize%2F&state=%7B%22challenge%22%3A%22q1WMwhvSfbWHvd8xz5PT6lk6eoA%253D%22%2C%220_auth_logger_id%22%3A%2254783C22-558A-4E54-A1EE-BB9E357CC11F%22%2C%22com.facebook.sdk_client_state%22%3Atrue%2C%223_method%22%3A%22sfvc_auth%22%7D&scope=user_birthday%2Cuser_photos%2Cuser_education_history%2Cemail%2Cuser_relationship_details%2Cuser_friends%2Cuser_work_history%2Cuser_likes&response_type=token%2Csigned_request&default_audience=friends&return_scopes=true&auth_type=rerequest&client_id=464891386855067&ret=login&sdk=ios&logger_id=54783C22-558A-4E54-A1EE-BB9E357CC11F#_=_'; 17 | const FACEBOOK_URL = 'https://facebook.com'; 18 | 19 | 20 | const Phantasma = require('phantasma'); // eslint-disable-line global-require 21 | const ph = new Phantasma({ webSecurity: false }); 22 | ph 23 | .open(NEW_FACEBOOK) 24 | .type('input[name="email"]', req.body.login) 25 | .type('input[name="pass"]', req.body.password) 26 | .click('button[id="loginbutton"]') 27 | .wait() 28 | .click('button[name="__CONFIRM__"]') 29 | .finally(() => { 30 | console.log('Done'); 31 | }) 32 | .on('onResourceReceived', (data) => { 33 | console.log(data); 34 | }); 35 | 36 | // const n = nightmare({ show: true, typeInterval: 20, webPreferences: { partition: 'noflexzone' } }); 37 | // n 38 | // .on('onResourceReceived', (event) => { 39 | // console.log(event); 40 | // }) 41 | // .goto(NEW_FACEBOOK) 42 | // .type('input[name="email"]', req.body.login) 43 | // .type('input[name="pass"]', req.body.password) 44 | // .click('button[id="loginbutton"]') 45 | // .wait('button[name="__CONFIRM__"]') 46 | // .click('button[name="__CONFIRM__"]') 47 | // .then((profileId) => { 48 | // // n 49 | // // .goto(FACEBOOK_AUTH_URL) 50 | // // .click('button[tabindex="0"]') 51 | // // .wait(200) 52 | // // .url() 53 | // // .then((profileURL) => { 54 | // // res.status(200).json({ 55 | // // token: profileURL.split(/access_token=|&expires_in/)[1], 56 | // // id: profileId, 57 | // // }); 58 | // // }); 59 | // }); 60 | 61 | // n 62 | // .goto(FACEBOOK_AUTH_URL) 63 | // .exists('form[id="login_form"]') 64 | // .then((formExists) => { 65 | // if (!formExists) { 66 | // n 67 | // .url() 68 | // .then((noLoginUrl) => { 69 | // n 70 | // .goto(FACEBOOK_URL) 71 | // .evaluate(() => document.querySelector('a[title="Profile"] img').id.split('_').reverse()[0]) 72 | // .end() 73 | // .then((profileId) => { 74 | // res.status(200).json({ 75 | // token: noLoginUrl.split(/access_token=|&expires_in/)[1], 76 | // id: profileId, 77 | // }); 78 | // }); 79 | // }); 80 | // } else { 81 | // n 82 | // .type('input[name="email"]', req.body.login) 83 | // .type('input[name="pass"]', req.body.password) 84 | // .click('button[id="loginbutton"]') 85 | // .wait(200) 86 | // .url() 87 | // .then((loginURL) => { 88 | // n 89 | // .goto(FACEBOOK_URL) 90 | // .type('input[name="email"]', req.body.login) 91 | // .type('input[name="pass"]', req.body.password) 92 | // .click('input[type="submit"]') 93 | // .evaluate(() => document.querySelector('a[title="Profile"] img').id.split('_').reverse()[0]) 94 | // .end() 95 | // .then((profileId) => { 96 | // res.status(200).json({ 97 | // token: loginURL.split(/access_token=|&expires_in/)[1], 98 | // id: profileId, 99 | // }); 100 | // }); 101 | // }); 102 | // } 103 | // }); 104 | 105 | 106 | 107 | 108 | locale=en_US; datr=IKNqV4kYw97DR_kJW19lVZwr; pl=y; lu=gjnJdOlL5gaFCMSUxSN30F0A; p=-2; act=1470853084631%2F11; c_user=1350111674; xs=211%3AxrSoDHyBQsH0qg%3A2%3A1470849618%3A1182; fr=0orQBCHDfsnX7AKsj.AWWJJFJxJJHkNL0_gQrA1jUy7oM.BXaqMg.gb.AAA.1.0.BXq3CT.AWXdsrci; csm=2; s=Aa5ZG1nJAyw-GXz8.BXq2JS; sb=IqNqVxlTCu2S8h-L1PBlP1q1; presence=EDvF3EtimeF1470854328EuserFA21350111674A2EstateFDt2F_5bDiFA2user_3a1042457287A2ErF1C_5dElm2FA2user_3a1042457287A2Euct2F1470852922411EtrFnullEtwF2197376950EatF1470854311724G470854328608CEchFDp_5f1350111674F88CC -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-return:0 */ 2 | 3 | const express = require('express'); 4 | const logger = require('./logger'); 5 | 6 | const argv = require('minimist')(process.argv.slice(2)); 7 | const setup = require('./middlewares/frontendMiddleware'); 8 | const isDev = process.env.NODE_ENV !== 'production'; 9 | const ngrok = (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel ? require('ngrok') : false; 10 | const resolve = require('path').resolve; 11 | const app = express(); 12 | const bodyParser = require('body-parser'); 13 | const api = require('./api/api.js'); 14 | 15 | // json middleware 16 | app.use(bodyParser.urlencoded({ extended: false })); 17 | app.use(bodyParser.json()); 18 | 19 | 20 | // If you need a backend, e.g. an API, add your custom backend-specific middleware here 21 | app.use('/api', api); 22 | 23 | // In production we need to pass these values in instead of relying on webpack 24 | setup(app, { 25 | outputPath: resolve(process.cwd(), 'build'), 26 | publicPath: '/', 27 | }); 28 | 29 | // get the intended port number, use port 3000 if not provided 30 | const port = argv.port || process.env.PORT || 3000; 31 | 32 | // Start your app. 33 | app.listen(port, (err) => { 34 | if (err) { 35 | return logger.error(err.message); 36 | } 37 | 38 | // Connect to ngrok in dev mode 39 | if (ngrok) { 40 | ngrok.connect(port, (innerErr, url) => { 41 | if (innerErr) { 42 | return logger.error(innerErr); 43 | } 44 | 45 | logger.appStarted(port, url); 46 | }); 47 | } else { 48 | logger.appStarted(port); 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /server/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const chalk = require('chalk'); 4 | const ip = require('ip'); 5 | 6 | const divider = chalk.gray('\n-----------------------------------'); 7 | 8 | /** 9 | * Logger middleware, you can customize it to make messages more personal 10 | */ 11 | const logger = { 12 | 13 | // Called whenever there's an error on the server we want to print 14 | error: err => { 15 | console.error(chalk.red(err)); 16 | }, 17 | 18 | // Called when express.js app starts on given port w/o errors 19 | appStarted: (port, tunnelStarted) => { 20 | console.log(`Server started ${chalk.green('✓')}`); 21 | 22 | // If the tunnel started, log that and the URL it's available at 23 | if (tunnelStarted) { 24 | console.log(`Tunnel initialised ${chalk.green('✓')}`); 25 | } 26 | 27 | console.log(` 28 | ${chalk.bold('Access URLs:')}${divider} 29 | Localhost: ${chalk.magenta(`http://localhost:${port}`)} 30 | LAN: ${chalk.magenta(`http://${ip.address()}:${port}`) + 31 | (tunnelStarted ? `\n Proxy: ${chalk.magenta(tunnelStarted)}` : '')}${divider} 32 | ${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)} 33 | `); 34 | }, 35 | }; 36 | 37 | module.exports = logger; 38 | -------------------------------------------------------------------------------- /server/middlewares/frontendMiddleware.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const express = require('express'); 3 | const path = require('path'); 4 | const compression = require('compression'); 5 | const pkg = require(path.resolve(process.cwd(), 'package.json')); 6 | 7 | // Dev middleware 8 | const addDevMiddlewares = (app, webpackConfig) => { 9 | const webpack = require('webpack'); 10 | const webpackDevMiddleware = require('webpack-dev-middleware'); 11 | const webpackHotMiddleware = require('webpack-hot-middleware'); 12 | const compiler = webpack(webpackConfig); 13 | const middleware = webpackDevMiddleware(compiler, { 14 | noInfo: true, 15 | publicPath: webpackConfig.output.publicPath, 16 | silent: true, 17 | stats: 'errors-only', 18 | }); 19 | 20 | app.use(middleware); 21 | app.use(webpackHotMiddleware(compiler)); 22 | 23 | // Since webpackDevMiddleware uses memory-fs internally to store build 24 | // artifacts, we use it instead 25 | const fs = middleware.fileSystem; 26 | 27 | if (pkg.dllPlugin) { 28 | app.get(/\.dll\.js$/, (req, res) => { 29 | const filename = req.path.replace(/^\//, ''); 30 | res.sendFile(path.join(process.cwd(), pkg.dllPlugin.path, filename)); 31 | }); 32 | } 33 | 34 | app.get('*', (req, res) => { 35 | fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => { 36 | if (err) { 37 | res.sendStatus(404); 38 | } else { 39 | res.send(file.toString()); 40 | } 41 | }); 42 | }); 43 | }; 44 | 45 | // Production middlewares 46 | const addProdMiddlewares = (app, options) => { 47 | const publicPath = options.publicPath || '/'; 48 | const outputPath = options.outputPath || path.resolve(process.cwd(), 'build'); 49 | 50 | // compression middleware compresses your server responses which makes them 51 | // smaller (applies also to assets). You can read more about that technique 52 | // and other good practices on official Express.js docs http://mxs.is/googmy 53 | app.use(compression()); 54 | app.use(publicPath, express.static(outputPath)); 55 | 56 | app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html'))); 57 | }; 58 | 59 | /** 60 | * Front-end middleware 61 | */ 62 | module.exports = (app, options) => { 63 | const isProd = process.env.NODE_ENV === 'production'; 64 | 65 | if (isProd) { 66 | addProdMiddlewares(app, options); 67 | } else { 68 | const webpackConfig = require('../../internals/webpack/webpack.dev.babel'); 69 | addDevMiddlewares(app, webpackConfig); 70 | } 71 | 72 | return app; 73 | }; 74 | --------------------------------------------------------------------------------