├── .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 |
7 | {_.map(photos, (photo, key) => (
8 | 
9 | ))}
10 |
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 (
12 | {cats.map((cat, index) => (
13 | - this.handleClick(index)} key={index}>
14 | {cat.name}
15 |
)
16 | )}
17 |
);
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 |
15 |
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 | :
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 ();
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 |
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 |

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 |

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 | [](http://waffle.io/chrisbodhi/mewment)
4 |
5 | 
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 | 
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 | );
75 | };
76 |
77 | Form.propTypes = {
78 | cats: React.PropTypes.array.isRequired,
79 | fields: React.PropTypes.object.isRequired,
80 | handleSubmit: React.PropTypes.func.isRequired,
81 | resetForm: React.PropTypes.func.isRequired,
82 | status: React.PropTypes.object.isRequired,
83 | submitting: React.PropTypes.bool.isRequired
84 | };
85 |
86 | const UploadForm = reduxForm(
87 | {
88 | form: 'file',
89 | fields: ['uid', 'catId', 'file', 'feed']
90 | },
91 | (state) => ({
92 | initialValues: {
93 | uid: state.user.uid,
94 | catId: state.status.catIndexForUpload
95 | }
96 | })
97 | )(Form);
98 |
99 | export default UploadForm;
100 |
--------------------------------------------------------------------------------
/app/modules/firebase-db.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import firebase from 'firebase';
3 |
4 | const db = firebase.database();
5 | const storageRef = process.env.NODE_ENV ? '' : firebase.storage().ref();
6 |
7 | function handleFileSelect(data, uid) {
8 | const file = data.file[0];
9 | const metadata = {
10 | contentType: file.type
11 | // todo: add to metadata the labels, confidence ratings from catternaut
12 | };
13 | const uidRef = storageRef.child(uid);
14 | const imageRef = uidRef.child(file.name);
15 | const uploadTask = imageRef.put(file, metadata);
16 |
17 | return new Promise((resolve, reject) => {
18 | uploadTask.on('state_changed', (snapshot) => {
19 | console.log('state change snapshot:', snapshot);
20 | }, (error) => {
21 | console.log('Error in uploading the image.');
22 | switch (error.code) {
23 | case 'storage/unauthorized':
24 | console.log('Error code:', error.code);
25 | break;
26 | case 'storage/unknown':
27 | console.log('Error code:', error.code, error.serverResponse);
28 | break;
29 | default:
30 | console.log('Error code:', error.code);
31 | break;
32 | }
33 | reject(error);
34 | }, () => {
35 | console.log('Uploaded', uploadTask.snapshot.totalBytes, 'bytes.');
36 | const image = uploadTask.snapshot.downloadURL;
37 | resolve(image);
38 | });
39 | });
40 | }
41 |
42 | export function saveProfileToFb(data) {
43 | const uid = firebase.auth().currentUser.providerData[0].uid;
44 | return handleFileSelect(data, uid)
45 | .then((avatar) => db
46 | .ref(`/cats/${uid}`)
47 | .once('value')
48 | .then((snapshot) => {
49 | // catId is the n-th cat attached to the UID
50 | const catId = _.size(snapshot.val());
51 | const { name, age, sex, color, about, wishlist } = data;
52 | db.ref(`/cats/${uid}/${catId}`)
53 | .set({ name, age, sex, color, about, avatar, wishlist });
54 | return { name, age, sex, color, about, avatar, wishlist };
55 | })
56 | .catch((err) => {
57 | throw new Error(`Error getting cat profile: ${err}`);
58 | })
59 | );
60 | }
61 |
62 | function merge(cats, snaps) {
63 | return _.map(cats, (cat, index) => _.assign(
64 | {},
65 | cat,
66 | snaps[index] || {}
67 | ));
68 | }
69 |
70 | function fetchProfiles(uid) {
71 | return db.ref(`/cats/${uid}`)
72 | .once('value')
73 | .then((snapshot) => snapshot.val());
74 | }
75 |
76 | function fetchPhotos(uid, cats) {
77 | return db.ref(`/photos/${uid}`)
78 | .once('value')
79 | .then((snapshot) => {
80 | const snaps = snapshot.val();
81 | return merge(cats, snaps);
82 | });
83 | }
84 |
85 | export function fetchCatsFromFb(uid) {
86 | return fetchProfiles(uid)
87 | .then((cats) => fetchPhotos(uid, cats))
88 | .catch((err) => {
89 | throw new Error(`Error getting cats or photos: ${err}`);
90 | });
91 | }
92 |
93 | export function addPhotoToFb(data) {
94 | const { uid, catId, feed } = data;
95 | return handleFileSelect(data, uid)
96 | .then((image) => {
97 | db.ref(`/photos/${uid}/${catId}/${feed}`)
98 | .push(image);
99 | return { image };
100 | })
101 | .catch((err) => {
102 | throw new Error(`Error uploading cat image: ${err}`);
103 | });
104 | }
105 |
--------------------------------------------------------------------------------
/app/reducers/index.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import { combineReducers } from 'redux';
3 | import { reducer as formReducer } from 'redux-form';
4 |
5 | import {
6 | SIGN_IN_WITH_FB,
7 | SIGN_OUT_OF_FB,
8 | RECEIVE_USER,
9 | ADD_PROFILE,
10 | CLEAR_PROFILE,
11 | ADD_CAT,
12 | FETCH_CATS_REQUEST,
13 | FETCH_CATS_SUCCESS,
14 | FETCH_CATS_ERR,
15 | SHOW_UPLOAD_FORM,
16 | ADD_TO_FEED_ERR
17 | } from '../actions';
18 |
19 | import { defaultStatus } from '../../test/test_helper';
20 |
21 | const initialUserState = {
22 | isFetching: false,
23 | didInvalidate: false,
24 | uid: ''
25 | };
26 |
27 | const invalidatedUser = {
28 | didInvalidate: true,
29 | displayName: '',
30 | email: '',
31 | isFetching: false,
32 | photoURL: '',
33 | providerId: '',
34 | uid: ''
35 | };
36 |
37 | function userAuth(state = initialUserState, action) {
38 | switch (action.type) {
39 | case SIGN_IN_WITH_FB:
40 | return _.assign(
41 | {},
42 | state,
43 | {
44 | user: {
45 | isFetching: true,
46 | didInvalidate: false
47 | }
48 | }
49 | );
50 | case RECEIVE_USER:
51 | return _.assign(
52 | {},
53 | state,
54 | {
55 | isFetching: false,
56 | didInvalidate: false
57 | },
58 | action.user
59 | );
60 | case SIGN_OUT_OF_FB:
61 | return _.assign(
62 | {},
63 | state,
64 | invalidatedUser
65 | );
66 | default:
67 | return state;
68 | }
69 | }
70 |
71 | function profile(state = {}, action) {
72 | switch (action.type) {
73 | case ADD_PROFILE:
74 | return _.assign(
75 | {},
76 | state,
77 | action.data
78 | );
79 | case CLEAR_PROFILE:
80 | return {};
81 | default:
82 | return state;
83 | }
84 | }
85 |
86 | function cats(state = [], action) {
87 | switch (action.type) {
88 | case ADD_CAT:
89 | return [...state, action.cat];
90 | case FETCH_CATS_SUCCESS:
91 | return [...action.catsFromFb];
92 | case SIGN_OUT_OF_FB:
93 | return [];
94 | default:
95 | return state;
96 | }
97 | }
98 |
99 | function status(state = defaultStatus, action) {
100 | const resetStatus = _.assign(
101 | {},
102 | defaultStatus
103 | );
104 | switch (action.type) {
105 | case FETCH_CATS_REQUEST:
106 | return _.assign(
107 | {},
108 | state,
109 | { fetchingCats: true }
110 | );
111 | case FETCH_CATS_SUCCESS:
112 | return resetStatus;
113 | case FETCH_CATS_ERR:
114 | return resetStatus;
115 | case SHOW_UPLOAD_FORM:
116 | return _.assign(
117 | {},
118 | state,
119 | {
120 | showUploadForm: true,
121 | catIndexForUpload: action.index
122 | }
123 | );
124 | case ADD_TO_FEED_ERR:
125 | return _.assign(
126 | {},
127 | state,
128 | { fetchedCatsError: true }
129 | );
130 | default:
131 | return state;
132 | }
133 | }
134 |
135 | const reducers = {
136 | user: userAuth,
137 | profile,
138 | form: formReducer,
139 | cats,
140 | status
141 | };
142 |
143 | // Note: this implicitly passes `state` and `action` args
144 | // to the reducer functions
145 | const app = combineReducers(reducers);
146 | export default app;
147 |
--------------------------------------------------------------------------------
/app/actions/index.js:
--------------------------------------------------------------------------------
1 | import { fbSignIn, fbSignOut } from '../modules/firebase-auth';
2 | import {
3 | addPhotoToFb,
4 | fetchCatsFromFb,
5 | saveProfileToFb
6 | } from '../modules/firebase-db';
7 | import { browserHistory } from 'react-router';
8 | // ACTION TYPES
9 | export const SIGN_IN_WITH_FB = 'SIGN_IN_WITH_FB';
10 | export const SIGN_OUT_OF_FB = 'SIGN_OUT_OF_FB';
11 | export const RECEIVE_USER = 'RECEIVE_USER';
12 | export const AUTH_ERROR = 'AUTH_ERROR';
13 |
14 | export const ADD_PROFILE = 'ADD_PROFILE';
15 | export const CLEAR_PROFILE = 'CLEAR_PROFILE';
16 | export const ADD_CAT = 'ADD_CAT';
17 |
18 | export const FETCH_CATS = 'FETCH_CATS';
19 | export const FETCH_CATS_REQUEST = 'FETCH_CATS_REQUEST';
20 | export const FETCH_CATS_SUCCESS = 'FETCH_CATS_SUCCESS';
21 | export const FETCH_CATS_ERR = 'FETCH_CATS_ERR';
22 |
23 | export const SHOW_UPLOAD_FORM = 'SHOW_UPLOAD_FORM';
24 | export const ADD_TO_FEED_REQUEST = 'ADD_TO_FEED_REQUEST';
25 | export const ADD_TO_FEED_ERR = 'ADD_TO_FEED_ERR';
26 |
27 | // ACTION CREATORS
28 |
29 | // Cat actions
30 | export function addCat(cat) {
31 | return {
32 | type: ADD_CAT,
33 | cat
34 | };
35 | }
36 |
37 | // start of fetching cats from firebase
38 | function fetchCatsRequest(uid) {
39 | return {
40 | type: FETCH_CATS_REQUEST,
41 | uid
42 | };
43 | }
44 |
45 | function fetchCatsSuccess(catsFromFb) {
46 | return {
47 | type: FETCH_CATS_SUCCESS,
48 | catsFromFb
49 | };
50 | }
51 |
52 | function fetchCatsErr(err) {
53 | return {
54 | type: FETCH_CATS_ERR,
55 | err
56 | };
57 | }
58 |
59 | export function fetchCats(uid) {
60 | return (dispatch) => {
61 | dispatch(fetchCatsRequest(uid));
62 | return fetchCatsFromFb(uid)
63 | .then((catsFromFb) => dispatch(fetchCatsSuccess(catsFromFb)))
64 | .catch((err) => dispatch(fetchCatsErr(err)));
65 | };
66 | }
67 | // end of fetching cats from firebase
68 |
69 | // start of adding cat photos
70 | export function showUploadForm(index) {
71 | return {
72 | type: SHOW_UPLOAD_FORM,
73 | index
74 | };
75 | }
76 |
77 | function addToFeedRequest({ uid, catId, feed }) {
78 | return {
79 | type: ADD_TO_FEED_REQUEST,
80 | uid,
81 | catId,
82 | feed
83 | };
84 | }
85 |
86 | function addToFeedErr(err) {
87 | return {
88 | type: ADD_TO_FEED_ERR,
89 | err
90 | };
91 | }
92 |
93 | export function addPhoto({ uid, catId, feed, file }) {
94 | return (dispatch) => {
95 | dispatch(addToFeedRequest({ uid, catId, feed }));
96 | return addPhotoToFb({ uid, catId, feed, file })
97 | .then(() => dispatch(fetchCats(uid)))
98 | .catch((err) => dispatch(addToFeedErr(err)));
99 | };
100 | }
101 | // end of adding cat photos
102 |
103 | // User actions
104 | function signInWithFb() {
105 | return {
106 | type: SIGN_IN_WITH_FB
107 | };
108 | }
109 |
110 | function signOutOfFb() {
111 | return {
112 | type: SIGN_OUT_OF_FB
113 | };
114 | }
115 |
116 | function receiveUser(user) {
117 | return {
118 | type: RECEIVE_USER,
119 | user
120 | };
121 | }
122 |
123 | export function authError(error) {
124 | return {
125 | type: AUTH_ERROR,
126 | error
127 | };
128 | }
129 |
130 | export function signIn() {
131 | return (dispatch) => {
132 | dispatch(signInWithFb);
133 | return fbSignIn()
134 | .then((user) => {
135 | dispatch(receiveUser(user));
136 | dispatch(fetchCats(user.uid));
137 | })
138 | .catch((err) => {
139 | throw new Error(`Err in signIn(): ${err}`);
140 | });
141 | };
142 | }
143 |
144 | export function signOut() {
145 | return (dispatch) => fbSignOut()
146 | .then(() => {
147 | dispatch(signOutOfFb());
148 | browserHistory.push('/');
149 | })
150 | .catch((err) => {
151 | throw new Error(`Err in signOut(): ${err}`);
152 | });
153 | }
154 |
155 | function addProfileToState() {
156 | return {
157 | type: ADD_PROFILE
158 | };
159 | }
160 |
161 | export function clearProfile() {
162 | return {
163 | type: CLEAR_PROFILE
164 | };
165 | }
166 |
167 | export function addProfile(data) {
168 | return (dispatch) => {
169 | dispatch(addProfileToState);
170 | return saveProfileToFb(data)
171 | .then((cat) => {
172 | dispatch(addCat(cat));
173 | dispatch(clearProfile);
174 | })
175 | .catch((err) => {
176 | throw new Error(`Err saving profile to Firebase: ${err}`);
177 | });
178 | };
179 | }
180 |
--------------------------------------------------------------------------------
/app/components/ProfileForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { reduxForm } from 'redux-form';
3 | import _ from 'lodash';
4 |
5 | import WishlistPicker from './WishlistPicker';
6 |
7 | const fields = [
8 | 'name',
9 | 'age',
10 | 'sex',
11 | 'color',
12 | 'about',
13 | 'file',
14 | 'wishlist.food',
15 | 'wishlist.litter',
16 | 'wishlist.toys'
17 | ];
18 |
19 |
20 | const FormContainer = (props) => {
21 | const {
22 | fields: {
23 | name,
24 | age,
25 | sex,
26 | color,
27 | about,
28 | file,
29 | wishlist
30 | },
31 | handleSubmit,
32 | onSubmit,
33 | products,
34 | resetForm,
35 | submitting
36 | } = props;
37 |
38 | return (
39 |
164 | );
165 | };
166 |
167 | FormContainer.propTypes = {
168 | fields: React.PropTypes.object.isRequired,
169 | handleSubmit: React.PropTypes.func.isRequired,
170 | products: React.PropTypes.array.isRequired,
171 | resetForm: React.PropTypes.func.isRequired,
172 | submitting: React.PropTypes.bool.isRequired
173 | };
174 |
175 | const ProfileForm = reduxForm({
176 | form: 'profile',
177 | fields
178 | })(FormContainer);
179 |
180 | export default ProfileForm;
181 |
--------------------------------------------------------------------------------
/test/action_spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import nock from 'nock';
3 | import configureMockStore from 'redux-mock-store';
4 | import thunk from 'redux-thunk';
5 |
6 | import { catProfile } from './test_helper';
7 |
8 | // import both types and functions
9 | import * as actions from '../app/actions';
10 |
11 | import { addPhotoToFb } from '../app/modules/firebase-db';
12 |
13 | const middlewares = [thunk];
14 | const mockStore = configureMockStore(middlewares);
15 |
16 | // todo: don't hardcode this; do better with mocks
17 | // todo: investigate use of https://github.com/mfncooper/mockery
18 | // or https://github.com/thlorenz/proxyquire
19 | // const catsFromFb = [
20 | // { about: 'I like laying in the sun and getting my ears rubbed. But don\'t take my kindness for weakness -- I\'ll ruin your shit if need be.', age: '12', color: 'Tabby', name: 'Bonnie' },
21 | // { about: 'things', age: '1', color: 'Null', name: 'A' },
22 | // { about: 'A big cuddle bug.', age: '13', color: 'White with orange spots', name: 'Ricky', sex: 'neutered' },
23 | // { about: 'Meow', age: '1', color: 'Q', name: 'T', sex: '' },
24 | // { about: '6', age: '8', avatar: 'https://firebasestorage.googleapis.com/v0/b/project-3398608299508035534.appspot.com/o/685030558304560?alt=media&token=cbd41a39-6521-4366-97f7-d7d8fd6b662b', color: '7', name: '9', sex: '' },
25 | // { about: 'm', age: '9', avatar: 'https://firebasestorage.googleapis.com/v0/b/project-3398608299508035534.appspot.com/o/685030558304560%2Fmake-brian-proud.png?alt=media&token=5f13a723-d555-441d-983c-4f3a5ec0676c', color: 'k', name: 'i', sex: '' },
26 | // { about: 'kjnkj', age: '88', avatar: 'https://firebasestorage.googleapis.com/v0/b/project-3398608299508035534.appspot.com/o/685030558304560%2Fmake-brian-proud.png?alt=media&token=b7702e8d-6bac-447c-8a58-f2ecf6f29396', color: 'unun', name: 'uu', sex: '' },
27 | // { about: '', age: '9999999999', color: '', name: '', sex: 'neutered' },
28 | // { about: 'p', age: '9', avatar: 'https://firebasestorage.googleapis.com/v0/b/project-3398608299508035534.appspot.com/o/685030558304560%2Fmake-brian-proud.png?alt=media&token=d6847c1e-c16a-4370-b2c8-2dab5c4d71fa', color: 'o', name: 'p', sex: 'spayed' },
29 | // { about: 'p', age: '3', avatar: 'https://firebasestorage.googleapis.com/v0/b/project-3398608299508035534.appspot.com/o/685030558304560%2Fmake-brian-proud.png?alt=media&token=efae1a89-02f7-4ff9-b39a-5af9665c53ac', color: 'o', name: 'p', sex: 'spayed' },
30 | // { about: 'rm', age: '0', avatar: 'https://firebasestorage.googleapis.com/v0/b/project-3398608299508035534.appspot.com/o/685030558304560%2Fmake-brian-proud.png?alt=media&token=fed4f00e-4057-43dc-aa67-d17debcd14cc', color: 'i', name: 'l', sex: 'spayed' },
31 | // { about: 'Yells. \n\n\nA lot.', age: '4', avatar: 'https://firebasestorage.googleapis.com/v0/b/project-3398608299508035534.appspot.com/o/685030558304560%2Fmake-brian-proud.png?alt=media&token=e20e1a89-c7e4-448c-a722-1aa1fa3822f8', color: 'Blackish', name: 'Argh', sex: 'spayed' },
32 | // { about: '9 to go!', age: '2', avatar: 'https://firebasestorage.googleapis.com/v0/b/project-3398608299508035534.appspot.com/o/685030558304560%2Fmake-brian-proud.png?alt=media&token=f088e08b-5796-487b-b317-c6639fb60d42', color: 'Lucky green', name: 'Grep', sex: 'spayed' },
33 | // { about: 'Like a tibula.', age: '98', avatar: 'https://firebasestorage.googleapis.com/v0/b/project-3398608299508035534.appspot.com/o/685030558304560%2Fmake-brian-proud.png?alt=media&token=d0176b8b-85eb-42e9-9408-2805a99d7c87', color: 'Tibby', name: 'Bob', sex: 'spayed' }
34 | // ];
35 | const catsFromFb = [catProfile];
36 | const uid = '685030558304560';
37 |
38 | describe('Actions', () => {
39 | describe('Cat Actions', () => {
40 | afterEach(() => {
41 | nock.cleanAll();
42 | });
43 |
44 | it('creates an action to add a cat', () => {
45 | const cat = {};
46 | const expectedAction = {
47 | type: actions.ADD_CAT,
48 | cat
49 | };
50 | expect(actions.addCat(cat)).toEqual(expectedAction);
51 | });
52 |
53 | xit('creates an action to fetch cats from Firebase', () => {
54 | nock('https://project-3398608299508035534.firebaseio.com')
55 | .get(`/cats/${uid}`)
56 | .reply(200, { catsFromFb });
57 |
58 | const expectedActions = [
59 | { type: actions.FETCH_CATS_REQUEST, uid },
60 | { type: actions.FETCH_CATS_SUCCESS, catsFromFb }
61 | ];
62 | const store = mockStore({ catsFromFb });
63 |
64 | return store.dispatch(actions.fetchCats(uid))
65 | .then(() => {
66 | expect(store.getActions()).toEqual(expectedActions);
67 | });
68 | });
69 |
70 | it('creates an action to show the file upload component', () => {
71 | const index = 1;
72 | const expectedAction = {
73 | type: actions.SHOW_UPLOAD_FORM,
74 | index
75 | };
76 | expect(actions.showUploadForm(index)).toEqual(expectedAction);
77 | });
78 |
79 | xit('creates ADD_TO_FEED_ERR when photo upload fails', () => {
80 | const feed = 'public';
81 | const catId = '1';
82 | nock('localhost:8080')
83 | .get('/todos')
84 | .reply(500, { body: { error: 'err string' } });
85 | const expectedActions = [
86 | { type: actions.ADD_TO_FEED_REQUEST },
87 | { type: actions.ADD_TO_FEED_ERR, err: 'err string' }
88 | ];
89 | const store = mockStore({ todos: [] });
90 |
91 | return store.dispatch(actions.addPhoto({ uid, catId, feed }))
92 | .then(() => {
93 | expect(store.getActions()).toEqual(expectedActions);
94 | });
95 | });
96 | });
97 |
98 | describe('Profile Actions', () => {
99 | it('creates an action to clear the profile', () => {
100 | const expectedAction = {
101 | type: actions.CLEAR_PROFILE
102 | };
103 | expect(actions.clearProfile()).toEqual(expectedAction);
104 | });
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/test/reducer_spec.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import expect from 'expect';
3 |
4 | import reducer from '../app/reducers';
5 | import { catProfile, defaultStatus as status } from './test_helper';
6 | import {
7 | ADD_PROFILE,
8 | CLEAR_PROFILE,
9 | ADD_CAT,
10 | FETCH_CATS_REQUEST,
11 | FETCH_CATS_SUCCESS,
12 | FETCH_CATS_ERR,
13 | SIGN_OUT_OF_FB,
14 | SHOW_UPLOAD_FORM,
15 | ADD_TO_FEED_ERR
16 | } from '../app/actions';
17 |
18 | const initialUser = {
19 | didInvalidate: false,
20 | isFetching: false,
21 | uid: ''
22 | };
23 | const initialProfile = {};
24 | const initialCats = [];
25 |
26 | const initialState = {
27 | user: initialUser,
28 | profile: initialProfile,
29 | cats: initialCats,
30 | form: {},
31 | status
32 | };
33 |
34 | describe('Reducers', () => {
35 | describe('profile reducer', () => {
36 | it('defaults to the initial state', () => {
37 | const nextState = reducer(undefined, {});
38 | expect(initialState).toEqual(nextState);
39 | });
40 |
41 | it('handles ADD_PROFILE', () => {
42 | const action = {
43 | type: ADD_PROFILE,
44 | data: catProfile
45 | };
46 | const nextState = reducer(initialState, action);
47 | expect(nextState.profile).toEqual(catProfile);
48 | });
49 |
50 | it('clears out the profile with CLEAR_PROFILE', () => {
51 | const profileState = reducer(initialState, {
52 | type: ADD_PROFILE,
53 | data: catProfile
54 | });
55 | const action = { type: CLEAR_PROFILE };
56 | const nextState = reducer(profileState, action);
57 | expect(nextState.profile).toEqual({});
58 | });
59 | });
60 |
61 | describe('cat reducer', () => {
62 | it('defaults to the initial state', () => {
63 | const nextFromInitState = reducer(undefined, {});
64 | expect(initialState).toEqual(nextFromInitState);
65 | });
66 |
67 | it('ADD_CAT moves data from profile to cats', () => {
68 | const profileState = reducer(initialState, {
69 | type: ADD_PROFILE,
70 | data: catProfile
71 | });
72 | const action = {
73 | type: ADD_CAT,
74 | cat: profileState.profile
75 | };
76 | const nextState = reducer(profileState, action);
77 | expect(nextState.cats.length).toBe(1);
78 | expect(nextState.cats[0]).toEqual(catProfile);
79 | });
80 |
81 | it('FETCH_CATS_SUCCESS overwrites the entire `cats` state', () => {
82 | const actionOne = {
83 | type: FETCH_CATS_SUCCESS,
84 | catsFromFb: [catProfile]
85 | };
86 | const initCatState = reducer(initialState, actionOne);
87 |
88 | const actionTwo = {
89 | type: FETCH_CATS_SUCCESS,
90 | catsFromFb: [catProfile]
91 | };
92 | const nextState = reducer(initCatState, actionTwo);
93 |
94 | expect(initCatState.cats.length).toEqual(nextState.cats.length);
95 | });
96 |
97 | it('FETCH_CATS_SUCCESS adds cat profile from Firebase to cats in state', () => {
98 | const fetchWin = {
99 | type: FETCH_CATS_SUCCESS,
100 | catsFromFb: [catProfile]
101 | };
102 | const nextState = reducer(initialState, fetchWin);
103 | expect(nextState.cats.length).toBe(1);
104 | expect(nextState.cats[0]).toEqual(catProfile);
105 | });
106 |
107 | it('SIGN_OUT_OF_FB empties the `cats` array in the state', () => {
108 | const fetchWin = {
109 | type: FETCH_CATS_SUCCESS,
110 | catsFromFb: [catProfile]
111 | };
112 | const catsState = reducer(initialState, fetchWin);
113 |
114 | const signOutAction = {
115 | type: SIGN_OUT_OF_FB
116 | };
117 | const nextState = reducer(catsState, signOutAction);
118 | expect(nextState.cats.length).toBe(0);
119 | });
120 | });
121 |
122 | describe('status reducer', () => {
123 | it('defaults to the initial statuses', () => {
124 | const nextState = reducer(undefined, {});
125 | expect(initialState).toEqual(nextState);
126 | });
127 |
128 | it('FETCH_CATS_REQUEST sets `fetchingCats` to true', () => {
129 | expect(initialState.status.fetchingCats).toBe(false);
130 | const fetchAction = {
131 | type: FETCH_CATS_REQUEST,
132 | uid: '666'
133 | };
134 | const nextState = reducer(initialState, fetchAction);
135 | expect(nextState.status.fetchingCats).toBe(true);
136 | });
137 |
138 | it('FETCH_CATS_SUCCESS sets `fetchingCats` to false', () => {
139 | const fetchAction = {
140 | type: FETCH_CATS_REQUEST,
141 | uid: '666'
142 | };
143 | const inProgressState = reducer(initialState, fetchAction);
144 | const fetchWin = {
145 | type: FETCH_CATS_SUCCESS,
146 | catsFromFb: [catProfile]
147 | };
148 | const nextState = reducer(inProgressState, fetchWin);
149 | expect(nextState.status.fetchingCats).toBe(false);
150 | });
151 |
152 | it('FETCH_CATS_ERR sets `fetchingCats` to false', () => {
153 | const fetchAction = {
154 | type: FETCH_CATS_REQUEST,
155 | uid: '666'
156 | };
157 | const inProgressState = reducer(initialState, fetchAction);
158 | const fetchFail = {
159 | type: FETCH_CATS_ERR,
160 | err: 'Too many cats :crying_cat_face:'
161 | };
162 | const nextState = reducer(inProgressState, fetchFail);
163 | expect(nextState.status.fetchingCats).toBe(false);
164 | });
165 |
166 | it('SHOW_UPLOAD_FORM set its respective status to true', () => {
167 | const index = 1;
168 | const action = {
169 | type: SHOW_UPLOAD_FORM,
170 | index
171 | };
172 | const nextState = reducer(initialState, action);
173 | expect(nextState.status.showUploadForm).toBe(true);
174 | expect(nextState.status.catIndexForUpload).toBe(index);
175 | });
176 |
177 | it('ADD_TO_FEED_ERR sets photoUploadError to true', () => {
178 | const action = {
179 | type: ADD_TO_FEED_ERR,
180 | err: 'err string'
181 | };
182 | const nextState = reducer(initialState, action);
183 | expect(nextState.status.fetchedCatsError).toBe(true);
184 | });
185 | });
186 | });
187 |
--------------------------------------------------------------------------------