├── .eslintignore ├── database.rules.json ├── public ├── images │ └── logo.png ├── styles │ ├── main.less │ ├── footer.less │ ├── base.less │ ├── index.less │ └── home.less └── index.html ├── .firebaserc ├── app ├── config │ ├── CONSTANTS │ ├── firebase.js │ └── routes.js ├── modules │ ├── wishlist_helper.js │ ├── localStorage.js │ ├── firebase-auth.js │ └── firebase-db.js ├── components │ ├── PhotoGrid.js │ ├── CatList.js │ ├── Footer.js │ ├── Main.js │ ├── Wishlist.js │ ├── WishlistPicker.js │ ├── SignIn.js │ ├── Profile.js │ ├── ProfilePreview.js │ ├── FullProfile.js │ ├── CatTile.js │ ├── Upload.js │ ├── Home.js │ ├── AddProfile.js │ ├── Header.js │ ├── UploadForm.js │ └── ProfileForm.js ├── App.js ├── reducers │ └── index.js └── actions │ └── index.js ├── .eslintrc.json ├── firebase.json ├── .gitignore ├── test ├── modules │ └── wishlist_helper_spec.js ├── components │ ├── Wishlist_spec.js │ ├── WishlistPicker_spec.js │ ├── PhotoGrid_spec.js │ ├── Header_spec.js │ ├── Profile_spec.js │ └── FullProfile_spec.js ├── test_helper.js ├── action_spec.js └── reducer_spec.js ├── webpack.config.js ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.html 2 | public/bundles.js -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | ".read": true, 3 | ".write": true 4 | } 5 | -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisbodhi/mewment/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "project-3398608299508035534" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/config/CONSTANTS: -------------------------------------------------------------------------------- 1 | // Public-facing constants 2 | export const AFFILIATE_CODE = '?amzn_code=666'; 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "rules": { 7 | "comma-dangle": "off", 8 | "new-cap": "off" 9 | }, 10 | "env": { 11 | "browser": true, 12 | "mocha": true, 13 | "node": true 14 | } 15 | } -------------------------------------------------------------------------------- /public/styles/main.less: -------------------------------------------------------------------------------- 1 | html, body, #root, #root > .main-container { 2 | height: 100%; 3 | } 4 | 5 | #main { 6 | min-height: 100%; 7 | margin-bottom: -4em; 8 | 9 | &:after { 10 | content: ""; 11 | display: block; 12 | height: 4em; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "public", 7 | "rewrites": [ 8 | { 9 | "source": "**", 10 | "destination": "/index.html" 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/config/firebase.js: -------------------------------------------------------------------------------- 1 | export const config = { 2 | apiKey: 'AIzaSyDBQ1JuBlrjwu1Gb8eFDR4aBIAmzov7iYg', 3 | authDomain: 'project-3398608299508035534.firebaseapp.com', 4 | databaseURL: 'https://project-3398608299508035534.firebaseio.com', 5 | storageBucket: 'project-3398608299508035534.appspot.com', 6 | }; 7 | -------------------------------------------------------------------------------- /app/modules/wishlist_helper.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { AFFILIATE_CODE } from '../config/CONSTANTS'; 3 | 4 | export function formValFormatter(input) { 5 | const [rawCategory, rawName, rawUrl] = input.split('|'); 6 | const category = _.capitalize(rawCategory); 7 | const name = rawName.split('_').map(_.capitalize).join(' '); 8 | const url = `${rawUrl}${AFFILIATE_CODE}`; 9 | return { category, name, url }; 10 | } 11 | -------------------------------------------------------------------------------- /app/components/PhotoGrid.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | const PhotoGrid = ({ photos }) => ( 5 |
6 | 11 |
12 | ); 13 | 14 | PhotoGrid.propTypes = { 15 | photos: React.PropTypes.object.isRequired 16 | }; 17 | 18 | export default PhotoGrid; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Environment Variables 7 | .env 8 | firebase-service-account-key.js 9 | 10 | # Compiled binary addons (http://nodejs.org/api/addons.html) 11 | build/Release 12 | 13 | # Build files 14 | public/bundle.js 15 | 16 | # Dependency directory 17 | node_modules 18 | 19 | # Optional npm cache directory 20 | .npm 21 | 22 | # Optional REPL history 23 | .node_repl_history 24 | 25 | # Local Notes 26 | state-shape.json 27 | -------------------------------------------------------------------------------- /public/styles/footer.less: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | height: 4em; 4 | background-color: @navy; 5 | 6 | .container { 7 | width: auto; 8 | max-width: 480px; 9 | 10 | ul { 11 | margin-top: 1.2em; 12 | text-align: center; 13 | 14 | li { 15 | display: inline; 16 | padding-right: 30px; 17 | 18 | a { 19 | color: @white; 20 | font-size: 1.2em; 21 | 22 | &:hover { 23 | cursor: pointer; 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/modules/wishlist_helper_spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | 3 | import { formValFormatter } from '../../app/modules/wishlist_helper'; 4 | import { AFFILIATE_CODE } from '../../app/config/CONSTANTS'; 5 | 6 | const displayedVal = 'food|fictional_wet_food|https://amzn.co/poiuytr'; 7 | const category = 'Food'; 8 | const productName = 'Fictional Wet Food'; 9 | const productUrl = 'https://amzn.co/poiuytr'; 10 | 11 | expect(formValFormatter(displayedVal)).toEqual({ 12 | category, 13 | name: productName, 14 | url: `${productUrl}${AFFILIATE_CODE}` 15 | }); 16 | -------------------------------------------------------------------------------- /app/modules/localStorage.js: -------------------------------------------------------------------------------- 1 | export const loadState = () => { 2 | try { 3 | const serializedState = localStorage.getItem('state'); 4 | if (serializedState === null) { 5 | return undefined; 6 | } 7 | return JSON.parse(serializedState); 8 | } catch (err) { 9 | return undefined; 10 | } 11 | }; 12 | 13 | export const saveState = (state) => { 14 | try { 15 | const serializedState = JSON.stringify(state); 16 | localStorage.setItem('state', serializedState); 17 | } catch (err) { 18 | console.log('err in saveState', err); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /app/components/CatList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { showUploadForm } from '../actions'; 3 | 4 | class CatList extends React.Component { 5 | handleClick(index) { 6 | this.props.dispatch(showUploadForm(index)); 7 | } 8 | 9 | render() { 10 | const { cats } = this.props; 11 | return (); 18 | } 19 | } 20 | 21 | CatList.propTypes = { 22 | cats: React.PropTypes.array.isRequired, 23 | dispatch: React.PropTypes.func.isRequired 24 | }; 25 | 26 | export default CatList; 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './app/App.js', 3 | output: { 4 | path: './public', 5 | filename: 'bundle.js' 6 | }, 7 | devtool: '#cheap-module-eval-source-map', 8 | module: { 9 | loaders: [ 10 | { 11 | test: /\.jsx?$/, 12 | exclude: /(node_modules|bower_components)/, 13 | loader: 'babel', 14 | query: { 15 | presets: ['react', 'es2015'] 16 | } 17 | }, 18 | { 19 | test: /\.less$/, 20 | exclude: /(node_modules|bower_components)/, 21 | loader: 'style!css!less' 22 | }, 23 | { 24 | test: /\.png$/, 25 | exclude: /(node_modules|bower_components)/, 26 | loader: 'url' 27 | } 28 | ] 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /app/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | const Footer = () => ( 5 | 23 | ); 24 | 25 | export default Footer; 26 | -------------------------------------------------------------------------------- /app/components/Main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { Header } from './Header'; 5 | import Footer from './Footer'; 6 | 7 | const MainContainer = ({ children }) => ( 8 |
9 | {/* Must pass in `children` to get active links in navbar */} 10 |
11 |
12 | {children} 13 |
14 |
16 | ); 17 | 18 | MainContainer.propTypes = { 19 | children: React.PropTypes.object.isRequired 20 | }; 21 | 22 | const mapStateToMainContainerProps = (state) => ({ user: state.user }); 23 | const Main = connect(mapStateToMainContainerProps)(MainContainer); 24 | 25 | export default Main; 26 | -------------------------------------------------------------------------------- /app/config/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Main from '../components/Main'; 3 | import Home from '../components/Home'; 4 | import Profile from '../components/Profile'; 5 | import AddProfile from '../components/AddProfile'; 6 | import FullProfile from '../components/FullProfile'; 7 | import Upload from '../components/Upload'; 8 | import { Route, IndexRoute } from 'react-router'; 9 | 10 | export default ( 11 | 12 | 13 | 14 | 15 | 16 | {/* default route */} 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /app/components/Wishlist.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Wishlist = ({ wishlist }) => ( 4 | wishlist ? 5 |
6 |
Wishlist
7 | 12 |
: 13 |
No wishlist -- sad cat face
14 | ); 15 | 16 | Wishlist.propTypes = { 17 | wishlist: React.PropTypes.shape({ 18 | toys: React.PropTypes.array.isRequired, 19 | food: React.PropTypes.array.isRequired, 20 | litter: React.PropTypes.array.isRequired, 21 | }).isRequired 22 | }; 23 | 24 | export default Wishlist; 25 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | *mew* 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/styles/base.less: -------------------------------------------------------------------------------- 1 | // Font setup 2 | @import url(//fonts.googleapis.com/css?family=Raleway); 3 | 4 | // To apply: h1 { @font-face(); } 5 | @font-face: { 6 | font-family: 'Raleway', sans-serif; 7 | font-style: normal; 8 | font-weight: normal; 9 | }; 10 | 11 | @full-width: { 12 | margin-left: -15px; 13 | margin-right: -15px; 14 | }; 15 | 16 | @home-bar: { 17 | height: 15px; 18 | }; 19 | 20 | // Variables 21 | @navy: #13425b; 22 | @purple: #773e6b; 23 | @white: #f2f2f2; 24 | @turquoise: #51a39d; 25 | @orange: #b6695b; 26 | @yellowish: #cdbb79; 27 | @fb-blue: #3b5998; 28 | 29 | @bevel: { 30 | -webkit-clip-path: polygon(20% 0%, 80% 0%, 100% 20%, 100% 80%, 80% 100%, 20% 100%, 0% 80%, 0% 20%); 31 | clip-path: polygon(20% 0%, 80% 0%, 100% 20%, 100% 80%, 80% 100%, 20% 100%, 0% 80%, 0% 20%); 32 | }; 33 | 34 | @circle: { 35 | -webkit-clip-path: circle(50% at 50% 50%); 36 | clip-path: circle(50% at 50% 50%); 37 | }; 38 | -------------------------------------------------------------------------------- /app/components/WishlistPicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | const WishlistPicker = (props) => { 5 | const { category, choices, wishlist } = props; 6 | return (
7 | {_.capitalize(category)} 8 | {_.map(choices, (choice, ind) => ( 9 | 21 | ) 22 | )} 23 |
); 24 | }; 25 | 26 | WishlistPicker.propTypes = { 27 | wishlist: React.PropTypes.object.isRequired, 28 | category: React.PropTypes.string.isRequired, 29 | choices: React.PropTypes.array.isRequired 30 | }; 31 | 32 | export default WishlistPicker; 33 | -------------------------------------------------------------------------------- /app/components/SignIn.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { signIn, signOut } from '../actions'; 4 | 5 | const SignInContainer = ({ user, dispatch, location }) => ( 6 |
{ 7 | user.uid 8 | ? () 11 | 12 | : ( dispatch(signIn())}> 13 | 14 | Sign in with Facebook 15 | ) 16 | }
17 | ); 18 | 19 | SignInContainer.propTypes = { 20 | user: React.PropTypes.shape({ 21 | uid: React.PropTypes.string.isRequired 22 | }).isRequired, 23 | dispatch: React.PropTypes.func.isRequired, 24 | location: React.PropTypes.string 25 | }; 26 | 27 | 28 | const mapStateToSignInContainerProps = (state) => ({ user: state.user }); 29 | const SignIn = connect(mapStateToSignInContainerProps)(SignInContainer); 30 | 31 | export default SignIn; 32 | -------------------------------------------------------------------------------- /test/components/Wishlist_spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import expect from 'expect'; 3 | import React from 'react'; 4 | import TestUtils from 'react-addons-test-utils'; 5 | import Wishlist from '../../app/components/Wishlist'; 6 | import { catProfile } from '../test_helper'; 7 | 8 | function wishlistSetup() { 9 | const props = { 10 | wishlist: catProfile.wishlist 11 | }; 12 | 13 | const renderer = TestUtils.createRenderer(); 14 | renderer.render(); 15 | const output = renderer.getRenderOutput(); 16 | 17 | return output; 18 | } 19 | 20 | describe('Component: Wishlist', () => { 21 | const output = wishlistSetup(); 22 | 23 | it('renders three unordered lists in a div', () => { 24 | expect(output.type).toBe('div'); 25 | expect(output.props.className).toBe('wishlist'); 26 | expect(output.props.children[1].props.children.length).toBe(3); 27 | 28 | const allUl = _.every(output.props.children[1].props.children, (kid) => ( 29 | kid.type === 'li' 30 | )); 31 | expect(allUl).toBe(true); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /public/styles/index.less: -------------------------------------------------------------------------------- 1 | // All app stylesheets 2 | @import "./main.less"; 3 | @import "./home.less"; 4 | @import "./footer.less"; 5 | 6 | // Mixins and such 7 | @import "./base.less"; 8 | // exposes 9 | // - font-face 10 | // - bevel 11 | // - circle 12 | // - color variables 13 | 14 | li { 15 | list-style: none; 16 | } 17 | 18 | .tile { 19 | width: 350px; 20 | float: left; 21 | margin: 15px 15px 0px 15px; 22 | box-shadow: 1px 1px 5px goldenrod; 23 | } 24 | 25 | .tile div, .tile img { 26 | padding: 10px; 27 | } 28 | 29 | a.navbar-brand { 30 | span { 31 | @font-face(); 32 | } 33 | } 34 | 35 | .cat-name { 36 | background-color: purple; 37 | color: white; 38 | font-weight: 600; 39 | font-size: 1.5em; 40 | padding: 10px; 41 | } 42 | 43 | form.private { 44 | background-color: gray; 45 | } 46 | 47 | form.public { 48 | background-color: rebeccapurple; 49 | color: white; 50 | } 51 | 52 | form.public button { 53 | background-color: black; 54 | } 55 | 56 | .hidden { 57 | display: none; 58 | } 59 | 60 | .shown { 61 | display: block; 62 | } 63 | -------------------------------------------------------------------------------- /test/test_helper.js: -------------------------------------------------------------------------------- 1 | export const catProfile = { 2 | name: 'Qwerty', 3 | age: 1, 4 | sex: 'Spayed', 5 | color: 'Black', 6 | about: 'Stinky', 7 | avatar: 'http://bit.ly/1rdk9Us', 8 | public: { 9 | firebaseKey1: 'https://placekitten.com/200/300', 10 | firebaseKey2: 'https://placekitten.com/300/300', 11 | firebaseKey3: 'https://placekitten.com/400/300', 12 | }, 13 | private: {}, 14 | wishlist: { 15 | toys: [ 16 | { name: 'Mouse', link: 'http://amzn.co/mousey' }, 17 | { name: 'Bird', link: 'http://amzn.co/birdy' } 18 | ], 19 | food: [ 20 | { name: 'dry food', link: 'http://amzn.co/dry' }, 21 | { name: 'canned food', link: 'http://amzn.co/canned' } 22 | ], 23 | litter: [ 24 | { name: 'littering', link: 'http://amzn.co/litter' } 25 | ] 26 | } 27 | }; 28 | 29 | export const defaultStatus = { 30 | photoIsUploading: false, 31 | photoUploadSuccess: false, 32 | photoUploadError: false, 33 | fetchingCats: false, 34 | fetchedCatsSuccess: false, 35 | fetchedCatsError: false, 36 | showUploadForm: false, 37 | catIndexForUpload: null 38 | }; 39 | -------------------------------------------------------------------------------- /app/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import thunkMiddleware from 'redux-thunk'; 4 | import createLogger from 'redux-logger'; 5 | 6 | import { Provider } from 'react-redux'; 7 | import { browserHistory, Router } from 'react-router'; 8 | import { createStore, applyMiddleware } from 'redux'; 9 | 10 | import routes from './config/routes'; 11 | import app from './reducers'; 12 | import { loadState, saveState } from './modules/localStorage'; 13 | 14 | require('../public/styles/index.less'); 15 | 16 | const loggerMiddleware = createLogger(); 17 | 18 | const persistedState = loadState(); 19 | const store = createStore( 20 | app, 21 | persistedState, 22 | applyMiddleware( 23 | thunkMiddleware, 24 | loggerMiddleware 25 | ) 26 | ); 27 | 28 | store.subscribe(() => { 29 | saveState({ 30 | user: store.getState().user, 31 | cats: store.getState().cats, 32 | status: store.getState().status 33 | }); 34 | }); 35 | 36 | ReactDOM.render( 37 | 38 | {routes} 39 | , 40 | document.getElementById('root') 41 | ); 42 | -------------------------------------------------------------------------------- /app/components/Profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | import CatTile from './CatTile'; 6 | 7 | export const CatProfiles = ({ cats }) => ( 8 |
    9 | {cats.map((cat, index) => CatTile(cat, index))} 10 |
11 | ); 12 | 13 | CatProfiles.propTypes = { 14 | cats: React.PropTypes.array.isRequired 15 | }; 16 | 17 | export const ProfileContainer = ({ user, cats }) => ( 18 | user.uid 19 | ? ( 20 |
21 |
22 | { 23 | cats && cats.length 24 | ? 25 | :

No cats yet!

26 | } 27 |
28 |
29 | Would you like to add a profile? 30 |
) 31 | : (
Sign in to access
) 32 | ); 33 | 34 | ProfileContainer.propTypes = { 35 | cats: React.PropTypes.array.isRequired, 36 | user: React.PropTypes.shape({ 37 | uid: React.PropTypes.string.isRequired 38 | }).isRequired 39 | }; 40 | 41 | const mapStateToProfileContainerProps = (state) => ({ user: state.user, cats: state.cats }); 42 | const Profile = connect(mapStateToProfileContainerProps)(ProfileContainer); 43 | 44 | export default Profile; 45 | -------------------------------------------------------------------------------- /app/components/ProfilePreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import _ from 'lodash'; 4 | 5 | const PureProfilePreview = (props) => ( 6 |
7 |

Name: {_.get(props, 'profile.name.value', '')}

8 |
9 |

Age: {_.get(props, 'profile.age.value', '')}

10 |
11 |

Breeder: {_.capitalize(_.get(props, 'profile.sex.value', ''))}

12 |
13 |

Color: {_.get(props, 'profile.color.value', '')}

14 |
15 |

Wishlist: 16 |
    {_.get(props, 'profile.wishlist', '') && 17 | _.map(props.profile.wishlist, (list) => ( 18 |
  • {list.value}
  • 19 | ))} 20 |
21 |

22 |
23 |

About: {_.get(props, 'profile.about.value', '')}

24 |
25 |
26 | ); 27 | 28 | PureProfilePreview.propTypes = { 29 | profile: React.PropTypes.shape({ 30 | wishlist: React.PropTypes.object.isRequired 31 | }).isRequired 32 | }; 33 | 34 | const mapStateToProfileContainerProps = (state) => ({ profile: state.form.profile }); 35 | const ProfilePreview = connect(mapStateToProfileContainerProps)(PureProfilePreview); 36 | 37 | export default ProfilePreview; 38 | -------------------------------------------------------------------------------- /app/components/FullProfile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import PhotoGrid from './PhotoGrid'; 5 | import Wishlist from './Wishlist'; 6 | 7 | export class FullProfileContainer extends React.Component { 8 | render() { 9 | const cat = this.props.cats[this.props.params.id]; 10 | return ( 11 |
12 |
13 | {`It's 14 |
15 |
16 |

Name: {cat.name}

17 |

18 |

Color: {cat.color}

19 |

Age: {cat.age}

20 |

Sex: {cat.sex}

21 |

22 |
23 | 24 |
25 | About: {cat.about} 26 |
27 | 28 |
29 | ); 30 | } 31 | } 32 | 33 | FullProfileContainer.propTypes = { 34 | cats: React.PropTypes.array.isRequired, 35 | params: React.PropTypes.shape({ 36 | id: React.PropTypes.string.isRequired 37 | }).isRequired 38 | }; 39 | 40 | const mapStateToFullProfileContainerProps = (state) => ({ cats: state.cats }); 41 | const FullProfile = connect(mapStateToFullProfileContainerProps)(FullProfileContainer); 42 | 43 | export default FullProfile; 44 | -------------------------------------------------------------------------------- /app/components/CatTile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import _ from 'lodash'; 4 | 5 | const CatTile = (cat, index) => (
  • 6 |
    7 | 8 |

    9 | {cat.name || 'Derp'} 10 |

    11 | 12 |
    Age: {cat.age}
    13 |
    Breeder: {cat.sex}
    14 |
    Color: {cat.color}
    15 |
    About: {cat.about}
    16 | {`It's 17 |
    Wishlist: 18 |
      19 | {_.map(cat.wishlist, (item, category) => ( 20 |
    • 21 | {/* Dat space after the below colon: terrible formatting hack */} 22 | {_.capitalize(category)}: {item.name} 26 |
    • 27 | ))} 28 |
    29 |
    30 |
    31 |
  • ); 32 | 33 | CatTile.propTypes = { 34 | index: React.PropTypes.number.isRequired, 35 | cat: React.PropTypes.shape({ 36 | name: React.PropTypes.string.isRequired, 37 | age: React.PropTypes.number.isRequired, 38 | color: React.PropTypes.string.isRequired, 39 | sex: React.PropTypes.string.isRequired, 40 | about: React.PropTypes.string.isRequired 41 | }).isRequired 42 | }; 43 | 44 | export default CatTile; 45 | -------------------------------------------------------------------------------- /app/modules/firebase-auth.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase'; 2 | import { config } from '../config/firebase'; 3 | import { serviceAccount } from '../../firebase-service-account-key'; 4 | 5 | let provider; 6 | 7 | if (process.env.NODE_ENV) { 8 | firebase.initializeApp({ 9 | serviceAccount, 10 | databaseURL: config.databaseURL 11 | }); 12 | } else { 13 | firebase.initializeApp(config); 14 | provider = new firebase.auth.FacebookAuthProvider(); 15 | } 16 | 17 | const db = firebase.database(); 18 | 19 | export function fbSignIn() { 20 | return firebase.auth().signInWithPopup(provider) 21 | .then((result) => { 22 | const user = result.user.providerData[0]; 23 | const uid = user.uid; 24 | 25 | // Create user in db if this one does not exist 26 | db.ref(`/users/${uid}`).once('value') 27 | .then((snapshot) => { 28 | if (!snapshot.val()) { 29 | const { displayName, email, photoURL, providerId } = user; 30 | db.ref(`/users/${uid}`).set({ displayName, email, photoURL, providerId, uid }); 31 | } 32 | }); 33 | return user; 34 | }) 35 | .catch((err) => { 36 | throw new Error(`error code: ${err.code}\n 37 | | errorMessage: ${err.message}\n 38 | | email: ${err.email}\n 39 | | credential: ${err.credential}`); 40 | }); 41 | } 42 | 43 | export function fbSignOut() { 44 | return firebase.auth().signOut() 45 | .then(() => ({})) 46 | .catch((err) => { 47 | throw new Error(`Trouble signing out: ${err}`); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /test/components/WishlistPicker_spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | import _ from 'lodash'; 5 | 6 | import WishlistPicker from '../../app/components/WishlistPicker'; 7 | 8 | function PickerSetup() { 9 | const props = { 10 | wishlist: { 11 | food: { 12 | value: '' 13 | }, 14 | litter: { 15 | value: '' 16 | }, 17 | toys: { 18 | value: '' 19 | } 20 | }, 21 | category: 'food', 22 | choices: [ 23 | { value: 'food-a' }, 24 | { value: 'food-b' } 25 | ] 26 | }; 27 | 28 | const renderer = TestUtils.createRenderer(); 29 | renderer.render(); 30 | const output = renderer.getRenderOutput(); 31 | 32 | return { 33 | props, 34 | output 35 | }; 36 | } 37 | 38 | describe('Component: WishlistPicker', () => { 39 | const { props, output } = PickerSetup(); 40 | const [legend, choices] = output.props.children; 41 | 42 | it('renders one fieldset', () => { 43 | expect(output.type).toBe('fieldset'); 44 | }); 45 | 46 | it('renders a legend for its fieldset', () => { 47 | expect(legend.type).toBe('legend'); 48 | expect(legend.props.children).toBe(_.capitalize(props.category)); 49 | }); 50 | 51 | it('renders a radio button for each choice', () => { 52 | const radioCheck = _.map(choices, (choice) => ( 53 | choice.props.children[0].props.type === 'radio' 54 | )); 55 | 56 | expect(radioCheck.length).toBe(props.choices.length); 57 | expect(_.every(radioCheck)).toBe(true); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/components/PhotoGrid_spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import expect from 'expect'; 3 | import React from 'react'; 4 | import TestUtils from 'react-addons-test-utils'; 5 | import PhotoGrid from '../../app/components/PhotoGrid'; 6 | import { catProfile } from '../test_helper'; 7 | 8 | function photoGridSetup() { 9 | const props = { 10 | photos: catProfile.public 11 | }; 12 | 13 | const renderer = TestUtils.createRenderer(); 14 | renderer.render(); 15 | const output = renderer.getRenderOutput(); 16 | 17 | return { 18 | props, 19 | output, 20 | renderer 21 | }; 22 | } 23 | 24 | describe('Component: PhotoGrid', () => { 25 | const { output } = photoGridSetup(); 26 | 27 | it('renders an unordered list in a div', () => { 28 | expect(output.type).toBe('div'); 29 | expect(output.props.children.type).toBe('ul'); 30 | }); 31 | 32 | it('renders all of the public photos', () => { 33 | const ul = output.props.children; 34 | expect(ul.props.children.length).toEqual(_.size(catProfile.public)); 35 | }); 36 | 37 | // expect all of the children types to be img 38 | it('renders only img types within the line items', () => { 39 | const lis = output.props.children.props.children; 40 | const imgs = _.map(lis, 'props.children.type'); 41 | expect(imgs.every((i) => i === 'img')).toBe(true); 42 | }); 43 | 44 | it('renders the keys from the public photos object', () => { 45 | const lis = output.props.children.props.children; 46 | const keys = _.map(lis, 'key'); 47 | _.forEach(keys, (key) => { 48 | expect(_.keys(catProfile.public)).toInclude(key); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /app/components/Upload.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import CatList from './CatList'; 5 | import UploadForm from './UploadForm'; 6 | import { addPhoto } from '../actions'; 7 | 8 | class UploadContainer extends React.Component { 9 | handleSubmit(e) { 10 | this.props.dispatch(addPhoto(e)); 11 | } 12 | 13 | render() { 14 | const { user, cats, dispatch, status } = this.props; 15 | return ( 16 | user.uid 17 | ? ( 18 |
    19 |
    20 |

    Your Cats!

    21 | { 22 | cats && cats.length 23 | ? 24 | :

    No cats yet!

    25 | } 26 |
    27 |
    28 | this.handleSubmit(e)} 30 | cats={cats} 31 | status={status} 32 | uid={user.uid} 33 | /> 34 |
    35 |
    ) 36 | : (
    Sign in to access
    ) 37 | ); 38 | } 39 | } 40 | 41 | UploadContainer.propTypes = { 42 | cats: React.PropTypes.array.isRequired, 43 | dispatch: React.PropTypes.func.isRequired, 44 | status: React.PropTypes.object.isRequired, 45 | user: React.PropTypes.shape({ 46 | uid: React.PropTypes.string.isRequired 47 | }).isRequired 48 | }; 49 | 50 | const mapStateToUploadContainerProps = (state) => ({ 51 | user: state.user, 52 | cats: state.cats, 53 | status: state.status 54 | }); 55 | const Upload = connect(mapStateToUploadContainerProps)(UploadContainer); 56 | 57 | export default Upload; 58 | -------------------------------------------------------------------------------- /app/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import SignIn from './SignIn'; 5 | 6 | const HomeContainer = ({ user }) => ( 7 | user.uid ? 8 |

    9 | Welcome back to Mewment, {user.displayName.split(' ')[0]}! 10 |
    todo: add some sort of home screen
    11 |

    : 12 |
    13 | 17 |

    The first social network just for cats

    18 |
    19 | 20 |
    21 |
    22 |
    23 |
    24 |
    25 |

    We use computer vision to make sure you only see cats

    26 |
    27 |
    28 |
    29 |

    No pesky humans to interrupt your cat photos

    30 |
    31 |
    32 |
    33 |

    Get suckers on the Internet to buy your cat toys & food

    34 |
    35 |
    36 |
    37 |
    38 | ); 39 | 40 | HomeContainer.propTypes = { 41 | user: React.PropTypes.shape({ 42 | uid: React.PropTypes.string.isRequired 43 | }).isRequired 44 | }; 45 | 46 | const mapStateToHomeContainerProps = (state) => ({ user: state.user }); 47 | const Home = connect(mapStateToHomeContainerProps)(HomeContainer); 48 | 49 | export default Home; 50 | -------------------------------------------------------------------------------- /app/components/AddProfile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import { connect } from 'react-redux'; 4 | 5 | import ProfileForm from './ProfileForm'; 6 | import ProfilePreview from './ProfilePreview'; 7 | import { addProfile as saveProfile } from '../actions'; 8 | 9 | class AddProfileContainer extends React.Component { 10 | handleSubmit(e) { 11 | const data = _.assign({}, e, { lastUpdated: Date.now() }); 12 | this.props.dispatch(saveProfile(data)); 13 | // dirty hack, replace it 14 | window.location.hash = window.location.hash.replace(/\/add/, ''); 15 | } 16 | 17 | render() { 18 | const products = [ 19 | { category: 'food', 20 | choices: [ 21 | { value: 'food-1' }, 22 | { value: 'food-2' } 23 | ] 24 | }, 25 | { category: 'litter', 26 | choices: [ 27 | { value: 'litter-1' }, 28 | { value: 'litter-2' } 29 | ] 30 | }, 31 | { category: 'toys', 32 | choices: [ 33 | { value: 'toys-1' }, 34 | { value: 'toys-2' } 35 | ] 36 | } 37 | ]; 38 | 39 | return ( 40 |
    41 | { 42 | this.props.user.uid 43 | ? ( 44 |
    45 |

    Profile

    46 | this.handleSubmit(e)} 48 | products={products} 49 | /> 50 | 51 |
    ) 52 | : (
    Sign in to access
    ) 53 | } 54 |
    55 | ); 56 | } 57 | } 58 | 59 | AddProfileContainer.propTypes = { 60 | dispatch: React.PropTypes.func.isRequired, 61 | user: React.PropTypes.shape({ 62 | uid: React.PropTypes.string.isRequired 63 | }).isRequired 64 | }; 65 | 66 | const mapStateToProfileContainerProps = (state) => ({ user: state.user }); 67 | const AddProfile = connect(mapStateToProfileContainerProps)(AddProfileContainer); 68 | 69 | export default AddProfile; 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mewment", 3 | "version": "0.0.1", 4 | "description": "A mashup of Tinder and Instagram. But just for cats.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "./node_modules/.bin/webpack --content-base public && firebase serve --port 3000", 8 | "deploy": "NODE_ENV=test npm test && ./node_modules/.bin/webpack --content-base public && firebase deploy", 9 | "dev": "webpack-dev-server --inline --hot --content-base public", 10 | "test": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-core/register \"test/**/*@(.js|.jsx)\"", 11 | "test:watch": "npm run test -- --watch" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/chrisbodhi/mewment" 16 | }, 17 | "keywords": [], 18 | "author": "Chris Boette", 19 | "license": "MIT", 20 | "dependencies": { 21 | "classnames": "^2.2.5", 22 | "cutestrap": "^1.3.1", 23 | "firebase": "^3.0.3", 24 | "history": "^1.13.1", 25 | "lodash": "^4.13.1", 26 | "react": "^0.14.3", 27 | "react-addons-test-utils": "^0.14.3", 28 | "react-dom": "^0.14.3", 29 | "react-redux": "^4.4.5", 30 | "react-router": "^2.0.0", 31 | "redux": "^3.5.2", 32 | "redux-form": "^5.2.5", 33 | "redux-logger": "^2.6.1", 34 | "redux-thunk": "^2.1.0" 35 | }, 36 | "devDependencies": { 37 | "babel-core": "^6.3.13", 38 | "babel-loader": "^6.2.0", 39 | "babel-preset-es2015": "^6.3.13", 40 | "babel-preset-react": "^6.3.13", 41 | "chai": "^3.5.0", 42 | "css-loader": "^0.23.1", 43 | "eslint-config-airbnb": "^9.0.1", 44 | "eslint-plugin-import": "^1.8.1", 45 | "eslint-plugin-jsx-a11y": "^1.2.2", 46 | "eslint-plugin-react": "^5.1.1", 47 | "expect": "^1.20.1", 48 | "file-loader": "^0.9.0", 49 | "less": "^2.7.1", 50 | "less-loader": "^2.2.3", 51 | "mocha": "^2.5.3", 52 | "nock": "^8.0.0", 53 | "redux-devtools": "^3.3.1", 54 | "redux-mock-store": "^1.1.1", 55 | "style-loader": "^0.13.1", 56 | "url-loader": "^0.5.7", 57 | "webpack": "^1.13.1" 58 | }, 59 | "engines": { 60 | "node": "6.2.0" 61 | }, 62 | "babel": { 63 | "presets": [ 64 | "es2015", 65 | "react" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/components/Header_spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | import _ from 'lodash'; 5 | 6 | import { HeaderContainer } from '../../app/components/Header'; 7 | 8 | function headerSetup(uid = '') { 9 | const props = { 10 | user: { uid } 11 | }; 12 | 13 | const renderer = TestUtils.createRenderer(); 14 | renderer.render(); 15 | const output = renderer.getRenderOutput(); 16 | 17 | return { 18 | props, 19 | output, 20 | renderer 21 | }; 22 | } 23 | 24 | describe('HeaderContainer', () => { 25 | describe('for a not-signed-in user', () => { 26 | it('renders an empty div', () => { 27 | const { output } = headerSetup(); 28 | 29 | expect(output.type).toBe('div'); 30 | expect(output.props).toExcludeKey('children'); 31 | }); 32 | }); 33 | 34 | describe('for a signed-in user', () => { 35 | it('renders a navbar', () => { 36 | const { output } = headerSetup('123'); 37 | 38 | expect(output.type).toBe('nav'); 39 | expect(output.props.className).toInclude('navbar'); 40 | expect(output.props.className).toInclude('navbar-default'); 41 | }); 42 | 43 | it('has a navbar that has a logo in it', () => { 44 | const { output } = headerSetup('123'); 45 | const brandObj = _.first(output.props.children.props.children); 46 | const logoLink = _.first(brandObj.props.children); 47 | const logo = _.last(logoLink.props.children); 48 | 49 | expect(logoLink.type).toBeA('function'); 50 | expect(logoLink.props.className).toBe('navbar-brand'); 51 | expect(logoLink.props.to).toBe('/'); 52 | 53 | expect(logo.type).toBe('img'); 54 | expect(logo.props.className).toBe('logo'); 55 | expect(logo.props.src).toInclude('logo.png'); 56 | }); 57 | 58 | it('has a navbar that also has links in it', () => { 59 | const { output } = headerSetup('123'); 60 | const links = _.last(output.props.children.props.children); 61 | const [mainUl, loginBtn] = links.props.children; 62 | const mainLinks = _.map(mainUl.props.children, l => l.props.children.props.to); 63 | 64 | expect(mainLinks) 65 | .toInclude('profiles') 66 | .toInclude('feed') 67 | .toInclude('matching') 68 | .toInclude('upload'); 69 | expect(loginBtn.props.children.type).toBe('li'); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /app/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | import SignIn from './SignIn'; 6 | 7 | export const HeaderContainer = ({ user }) => ( 8 | user.uid ? 9 | : 66 |
    67 | ); 68 | 69 | HeaderContainer.propTypes = { 70 | user: React.PropTypes.shape({ 71 | uid: React.PropTypes.string.isRequired 72 | }).isRequired 73 | }; 74 | 75 | const mapStateToHeaderContainerProps = (state) => ({ user: state.user }); 76 | export const Header = connect(mapStateToHeaderContainerProps)(HeaderContainer); 77 | -------------------------------------------------------------------------------- /test/components/Profile_spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | import { ProfileContainer, CatProfiles } from '../../app/components/Profile'; 5 | import { catProfile } from '../test_helper'; 6 | 7 | function profileSetup(uid) { 8 | const props = { 9 | cats: [], 10 | user: { uid } 11 | }; 12 | 13 | const renderer = TestUtils.createRenderer(); 14 | renderer.render(); 15 | const output = renderer.getRenderOutput(); 16 | 17 | return { 18 | props, 19 | output, 20 | renderer 21 | }; 22 | } 23 | 24 | function catSetup() { 25 | const props = { 26 | cats: [catProfile] 27 | }; 28 | 29 | const renderer = TestUtils.createRenderer(); 30 | renderer.render(); 31 | const output = renderer.getRenderOutput(); 32 | 33 | return { 34 | props, 35 | output, 36 | renderer 37 | }; 38 | } 39 | 40 | describe('Components', () => { 41 | describe('ProfileContainer', () => { 42 | it('should render a div containing three elements for a signed-in user', () => { 43 | const { output } = profileSetup('1234'); 44 | expect(output.type).toBe('div'); 45 | expect(output.props.children.length).toBe(3); 46 | 47 | const [section, hr, Link] = output.props.children; 48 | expect(section.props.className).toBe('grid'); 49 | expect(Link.props.to).toBe('/profiles/add'); 50 | expect(hr.type).toBe('hr'); 51 | }); 52 | 53 | it('should ask an anon user to sign in', () => { 54 | const { output } = profileSetup(''); 55 | expect(output.type).toBe('div'); 56 | expect(output.props.children).toBe('Sign in to access'); 57 | }); 58 | }); 59 | 60 | describe('CatProfiles', () => { 61 | const { output } = catSetup(); 62 | it('should contain an unordered list', () => { 63 | expect(output.type).toBe('ul'); 64 | }); 65 | 66 | it('should contain just one cat', () => { 67 | expect(output.props.children.length).toBe(1); 68 | }); 69 | 70 | it('should contain the correct name, age, sex, color, about, and avatar url', () => { 71 | const catLi = output.props.children[0].props.children.props.children; 72 | expect(catLi.length).toBe(7); 73 | 74 | expect(catLi[0].props.children.props.children).toInclude('Qwerty'); 75 | expect(catLi[1].props.children).toInclude('1'); 76 | expect(catLi[2].props.children).toInclude('Spayed'); 77 | expect(catLi[3].props.children).toInclude('Black'); 78 | expect(catLi[4].props.children).toInclude('Stinky'); 79 | expect(catLi[5].props.src).toBe('http://bit.ly/1rdk9Us'); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /public/styles/home.less: -------------------------------------------------------------------------------- 1 | // will have access to 2 | // - font-face 3 | // - bevel 4 | // - circle 5 | // - color variables 6 | 7 | // for the non-signed-in user 8 | #splash { 9 | background-color: @white; 10 | @full-width(); 11 | 12 | #header { 13 | background-color: @navy; 14 | padding: 40px 0; 15 | 16 | span#logo { 17 | background-image: url('../images/logo.png'); 18 | background-repeat: no-repeat; 19 | height: 128px; 20 | } 21 | 22 | h1 { 23 | @font-face(); 24 | color: @white; 25 | font-size: 6em; 26 | } 27 | } 28 | 29 | h3 { 30 | @font-face(); 31 | padding: 30px 0; 32 | color: @navy; 33 | } 34 | 35 | // Adapted from https://github.com/lipis/bootstrap-social/blob/gh-pages/bootstrap-social.less 36 | @bs-height-base: (12px + 12px * 2); 37 | .signin-home a { 38 | color: @white; 39 | background-color: @fb-blue; 40 | position: relative; 41 | padding-left: (@bs-height-base + 12px); 42 | text-align: left; 43 | white-space: nowrap; 44 | overflow: hidden; 45 | text-overflow: ellipsis; 46 | > :first-child { 47 | position: absolute; 48 | left: 0; 49 | top: 1; 50 | bottom: 0; 51 | width: @bs-height-base; 52 | line-height: (@bs-height-base + 2); 53 | font-size: 1.6em; 54 | text-align: center; 55 | border-right: 1px solid rgba(0, 0, 0, 0.2); 56 | } 57 | &:active { 58 | background-color: @yellowish; 59 | color: @navy; 60 | } 61 | &:hover { 62 | animation:rainbow 1s infinite; 63 | } 64 | @keyframes rainbow { 65 | 0% {color: #ff0000;} 66 | 10% {color: #ff8000;} 67 | 20% {color: #ffff00;} 68 | 30% {color: #80ff00;} 69 | 40% {color: #00ff00;} 70 | 50% {color: #00ff80;} 71 | 60% {color: #00ffff;} 72 | 70% {color: #0080ff;} 73 | 80% {color: #0000ff;} 74 | 90% {color: #8000ff;} 75 | 100% {color: #ff0080;} 76 | } 77 | } 78 | 79 | #three-selling-points { 80 | margin: 0 auto; 81 | padding: 45px 0; 82 | 83 | .box { 84 | margin: 0 auto; 85 | height: 150px; 86 | max-width: 250px; 87 | background-color: @navy; 88 | background-image: url('http://placekitten.com/250/150'); 89 | background-repeat: no-repeat; 90 | } 91 | 92 | p { 93 | @font-face(); 94 | color: @navy; 95 | max-width: 250px; 96 | margin: 10px auto 0 auto; 97 | line-height: 1.4; 98 | font-size: 1.3em; 99 | text-align: center; 100 | } 101 | } 102 | } 103 | 104 | /* 105 | @navy 106 | @yellowish 107 | @orange 108 | @turquoise 109 | @purple 110 | @white 111 | @fb-blue 112 | */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Mewment 2 | 3 | [![Stories in Ready](https://badge.waffle.io/chrisbodhi/mewment.png?label=ready&title=Ready)](http://waffle.io/chrisbodhi/mewment) 4 | 5 | ![mew](http://www.glossophilia.org/wp-content/uploads/catmew.jpg) 6 | 7 | _Kinda like a mashup of Tinder and Instagram, but for cats — phase II will ~~mash in Uber~~ [this will be sort of hard to test without Uber operating in ATX right now, so phase II is up in the air `/shrug` ]_ 8 | 9 | ##Running Locally 10 | - Clone the repo using your preferred method. 11 | - Maybe `git clone git@github.com:chrisbodhi/mewment.git` from your command line? 12 | - Run `npm install` in the project's root directory. 13 | - Run `npm start` to build using Webpack and then serve up the content on port 3000. 14 | - Hop over to [http://localhost:3000](http://localhost:3000) to share a Mewment. 15 | 16 | ![mew, too](https://s-media-cache-ak0.pinimg.com/736x/e6/f9/65/e6f9651fc851ac860c60af7dee79c26a.jpg) 17 | 18 | ##Dev Notes 19 | - Install both webpack-dev-server [`npm install -g webpack-dev-server`] and Mocha [`npm install -g mocha`] globally. 20 | - Clone the repo using your preferred method. 21 | - Maybe `git clone git@github.com:chrisbodhi/mewment.git` from your command line? Or `git clone chrisbodhi/mewment` if you're using [Hub](https://hub.github.com). 22 | - Run `npm install` in the project's root directory. 23 | - Run `npm test` to run the whole test suite once or `npm run test:watch` to start watching the test directory. 24 | - `npm run dev` to start the Webpack dev server with ~~hawt~~ hot reloading of that `bundle.js` file in the `public` directory. 25 | - Go to [http://localhost:8080](http://localhost:8080) to create some Mewments. 26 | 27 | ##How to Deploy 28 | - Install Firebase CLI globally [`npm install -g firebase-tools`]. 29 | - Run `firebase login` and, um, login. 30 | - Run `npm deploy` and :tada: 31 | - Unless any of your tests are failing. Then it's all 😿 32 | 33 | ##Resources 34 | This project is using React & Redux, along with a host of other tools standard to that ecosystem: Webpack, Babel for transpiling ES2015, & ESLint. Over time, I've found the following resources helpful: 35 | 36 | ####React 37 | - [Thinking in React](https://facebook.github.io/react/docs/thinking-in-react.html) from FB || _free_ 38 | - [Build Your First React.js App](https://egghead.io/series/build-your-first-react-js-application) from Egghead -- also includes Webpack config and an ES2015 refactor || _sub required_ 39 | 40 | ####Redux 41 | - [Redux video course](https://egghead.io/series/getting-started-with-redux) from the creator of Redux, Dan Abramov. || _free_ 42 | - [Redux ~~encyclopedia~~ docs](http://redux.js.org/docs/basics/index.html) || _free_ 43 | 44 | ####Firebase 45 | - [Getting Started with Firebase](https://firebase.google.com/docs/web/setup) || _free_ -------------------------------------------------------------------------------- /test/components/FullProfile_spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | 5 | import { FullProfileContainer as FullProfile } from '../../app/components/FullProfile'; 6 | 7 | import { catProfile } from '../test_helper'; 8 | 9 | function profileSetup(uid) { 10 | const props = { 11 | cat: catProfile, 12 | cats: [catProfile], 13 | params: { id: '0' }, 14 | user: { uid } 15 | }; 16 | 17 | const renderer = TestUtils.createRenderer(); 18 | renderer.render(); 19 | const output = renderer.getRenderOutput(); 20 | 21 | return { 22 | props, 23 | output, 24 | renderer 25 | }; 26 | } 27 | 28 | describe('Component: FullProfile', () => { 29 | const { output } = profileSetup('1234'); 30 | const [ 31 | mainImgDiv, 32 | heading, 33 | photoGrid, 34 | profile, 35 | wishlist 36 | ] = output.props.children; 37 | 38 | it('renders the main profile photo div', () => { 39 | expect(mainImgDiv.type).toBe('div'); 40 | expect(mainImgDiv.props.id).toBe('mainImage'); 41 | expect(mainImgDiv.props.children.type).toBe('img'); 42 | }); 43 | 44 | it('renders the main profile img', () => { 45 | const mainImg = mainImgDiv.props.children; 46 | expect(mainImg.props.alt).toInclude(catProfile.name); 47 | expect(mainImg.props.src).toBe(catProfile.avatar); 48 | }); 49 | 50 | it('renders a cat\'s top-level info', () => { 51 | expect(heading.type).toBe('div'); 52 | expect(heading.props.id).toBe('heading'); 53 | }); 54 | 55 | it('renders the name in the heading', () => { 56 | const name = heading.props.children[0]; 57 | 58 | expect(name.type).toBe('h1'); 59 | expect(name.props.className).toBe('name'); 60 | expect(name.props.children).toInclude(catProfile.name); 61 | }); 62 | 63 | it('renders the descriptors in the heading', () => { 64 | const descriptors = heading.props.children[1]; 65 | 66 | expect(descriptors.type).toBe('h3'); 67 | expect(descriptors.props.className).toBe('descriptors'); 68 | 69 | const [color, age, sex] = descriptors.props.children; 70 | 71 | expect(color.props.className).toBe('color'); 72 | expect(color.props.children).toInclude(catProfile.color); 73 | 74 | expect(age.props.className).toBe('age'); 75 | expect(age.props.children).toInclude(catProfile.age); 76 | 77 | expect(sex.props.className).toBe('sex'); 78 | expect(sex.props.children).toInclude(catProfile.sex); 79 | }); 80 | 81 | // Testing of custom components happens in their respective specs 82 | it('renders all of the cat\'s photos', () => { 83 | expect(photoGrid).toExist(); 84 | expect(photoGrid.type).toBeA('function'); 85 | }); 86 | 87 | it('renders the cat\'s profile', () => { 88 | expect(profile.type).toBe('div'); 89 | expect(profile.props.id).toBe('profile'); 90 | expect(profile.props.children).toInclude(catProfile.about); 91 | }); 92 | 93 | // Same 94 | it('renders the shopping wishlist', () => { 95 | expect(wishlist).toExist(); 96 | expect(wishlist.type).toBeA('function'); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /app/components/UploadForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { reduxForm } from 'redux-form'; 3 | import classNames from 'classnames'; 4 | 5 | const Form = (props) => { 6 | // eslint note: `uid` & `id` used with `initialValues` in reduxForm() 7 | const { 8 | fields: { 9 | uid, // eslint-disable-line 10 | catId, // eslint-disable-line 11 | file, 12 | feed 13 | }, 14 | handleSubmit, 15 | resetForm, 16 | submitting, 17 | status, 18 | cats 19 | } = props; 20 | 21 | // Sets the class -- and therefore styling -- of the image upload form 22 | const formClass = classNames(feed.value, 23 | { shown: status.showUploadForm }, 24 | { hidden: !status.showUploadForm } 25 | ); 26 | 27 | const index = status.catIndexForUpload; 28 | const name = index !== null ? cats[index].name : ''; 29 | return ( 30 |
    31 |
    32 |
    33 | 34 | 40 |
    41 |
    42 | 51 | 60 |
    61 |
    62 |