13 | { _.map( this.props.images, this.renderImage ) }
14 |
15 | );
16 | },
17 |
18 | renderImage( image ) {
19 | return (
20 |
55 | No recipes found. Use the calculator to add recipes to your profile.
56 |
57 | );
58 | }
59 | },
60 |
61 | renderRecipe( recipe ) {
62 | return (
63 |
55 | No Recipes found. Add other member's recipes to your favourites to view them here.
56 |
57 | );
58 | }
59 | },
60 |
61 | renderRecipe( recipe ) {
62 | return (
63 |
36 | { this.renderLoading() }
37 | { this.renderNewsFeed() }
38 |
39 | );
40 | },
41 |
42 | renderLoading() {
43 | if ( !(this.state.feed.length) ) {
44 | return
52 | { _.map( this.state.feed, this.renderFeedItem ) }
53 | { this.paginator() }
54 |
55 | );
56 | }
57 | },
58 |
59 | renderFeedItem( feedItem ) {
60 | return (
61 |
44 | { _.map( this.state.comments, this.renderComment, this ) }
45 |
46 | );
47 | }
48 | },
49 |
50 | renderComment( comment ) {
51 | let links;
52 | let route;
53 |
54 | route = {
55 | oils: 'oil',
56 | recipes: 'recipe',
57 | status_updates: getStatusUpdateLinks,
58 | recipe_journals: getRecipeJournalsLinks
59 | }[ comment.commentable_type ];
60 |
61 | if ( _.isFunction( route ) ) {
62 | links = route();
63 | } else {
64 | links = [ ");
11 | $alert.attr("class", "bootstrap-growl alert");
12 | if (options.type) {
13 | $alert.addClass("alert-" + options.type);
14 | }
15 | if (options.allowDismiss) {
16 | $alert.addClass("alert-dismissible");
17 | $alert.append("
");
18 | }
19 | $alert.append(message);
20 | if (options.top_offset) {
21 | options.offset = {
22 | from: "top",
23 | amount: options.top_offset
24 | };
25 | }
26 | offsetAmount = options.offset.amount;
27 | $(".bootstrap-growl").each(function() {
28 | return offsetAmount = Math.max(offsetAmount, parseInt($(this).css(options.offset.from)) + $(this).outerHeight() + options.stackupSpacing);
29 | });
30 | css = {
31 | "position": (options.ele === "body" ? "fixed" : "absolute"),
32 | "margin": 0,
33 | "z-index": "9999",
34 | "display": "none"
35 | };
36 | css[options.offset.from] = offsetAmount + "px";
37 | $alert.css(css);
38 | if (options.width !== "auto") {
39 | $alert.css("width", options.width + "px");
40 | }
41 | $(options.ele).append($alert);
42 | switch (options.align) {
43 | case "center":
44 | $alert.css({
45 | "left": "50%",
46 | "margin-left": "-" + ($alert.outerWidth() / 2) + "px"
47 | });
48 | break;
49 | case "left":
50 | $alert.css("left", "20px");
51 | break;
52 | default:
53 | $alert.css("right", "20px");
54 | }
55 | $alert.fadeIn();
56 | if (options.delay > 0) {
57 | $alert.delay(options.delay).fadeOut(function() {
58 | return $(this).alert("close");
59 | });
60 | }
61 | return $alert;
62 | };
63 |
64 | $.bootstrapGrowl.default_options = {
65 | ele: "body",
66 | type: "info",
67 | offset: {
68 | from: "top",
69 | amount: 20
70 | },
71 | align: "right",
72 | width: 250,
73 | delay: 4000,
74 | allowDismiss: true,
75 | stackupSpacing: 10
76 | };
77 |
78 | }).call(this);
--------------------------------------------------------------------------------
/src/app/views/recipeJournal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Reflux from 'reflux';
3 | import { Link, State } from 'react-router';
4 |
5 | import recipeActions from 'actions/recipe';
6 |
7 | import recipeStore from 'stores/recipe';
8 | import recipeJournalStore from 'stores/recipeJournal';
9 | import recipeJournalCommentStore from 'stores/journalComments';
10 |
11 | import Commentable from 'components/commentable';
12 | import RecipeJournalItem from 'components/recipeJournalItem';
13 | import Spinner from 'components/spinner';
14 |
15 | export default React.createClass( {
16 |
17 | mixins: [
18 | State,
19 | Reflux.connect( recipeStore, 'recipe' ),
20 | Reflux.connect( recipeJournalStore, 'journal' )
21 | ],
22 |
23 | componentDidMount() {
24 | recipeJournalStore.reset();
25 |
26 | recipeActions.getRecipeById( this.getParams().recipeId )
27 | .tap( recipe => recipeActions.getRecipeJournal( recipe, { id: this.getParams().journalId } ) )
28 | .then( recipe => recipeActions.getRecipeJournalComments( recipe, { id: this.getParams().journalId } ) );
29 | },
30 |
31 | render() {
32 | let recipe = this.state.recipe;
33 |
34 | document.title = 'Soapee - Journal';
35 |
36 | return (
37 |
38 |
39 | - Home
40 | - Recipes
41 | { Number(recipe.getModelValue( 'id' )) > 0 && - { recipe.getModelValue( 'name' ) }
}
42 | - Viewing Recipe Journal
43 |
44 |
45 | { this.renderLoading() }
46 | { this.renderJournal() }
47 |
48 | );
49 | },
50 |
51 | renderJournal() {
52 | let journal = this.state.journal;
53 | let recipe = this.state.recipe.recipe;
54 |
55 | if ( journal ) {
56 | return (
57 |
58 |
59 |
65 |
66 |
67 |
68 |
71 |
72 | );
73 | }
74 | },
75 |
76 | renderLoading() {
77 | if ( !(this.state.journal) ) {
78 | return (
79 |
80 | );
81 | }
82 | }
83 |
84 | } );
--------------------------------------------------------------------------------
/src/app/views/recipes.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import Reflux from 'reflux';
4 | import DocMeta from 'react-doc-meta';
5 | import Pager from 'react-pager';
6 |
7 | import recipeActions from 'actions/recipe';
8 | import recipesStore from 'stores/recipes';
9 |
10 | import RecipeListItem from 'components/recipeListItem';
11 | import Spinner from 'components/spinner';
12 |
13 | export default React.createClass( {
14 |
15 | mixins: [
16 | Reflux.connect( recipesStore, 'recipes' )
17 | ],
18 |
19 | getInitialState() {
20 | return {
21 | activePage: 0
22 | };
23 | },
24 |
25 | componentDidMount() {
26 | document.title = 'Soapee - Recipes';
27 | recipeActions.getRecipes();
28 | },
29 |
30 | render() {
31 | return (
32 |
33 |
34 |
Latest Public Soap Recipes
35 |
36 | { this.renderLoading() }
37 | { this.renderRecipes() }
38 |
39 | { this.paginator() }
40 |
41 | );
42 | },
43 |
44 | renderLoading() {
45 | if ( !(this.state.recipes.length > 0) ) {
46 | return
;
47 | }
48 | },
49 |
50 | renderRecipes() {
51 | if ( this.state.recipes.length > 0 ) {
52 | return _( this.state.recipes )
53 | .sortBy( 'created_at' )
54 | .reverse()
55 | .map( this.renderRecipe )
56 | .value();
57 | }
58 | },
59 |
60 | paginator() {
61 | if ( this.state.recipes.length > 0 ) {
62 | return (
63 |
69 | );
70 | }
71 | },
72 |
73 | onPageChanged: function( page ) {
74 | this.setState( {
75 | activePage: page
76 | } );
77 |
78 | recipeActions.getRecipes( {
79 | page
80 | } );
81 | },
82 |
83 | renderRecipe( recipe ) {
84 | return (
85 |
86 |
90 |
91 | );
92 | },
93 |
94 | tags() {
95 | let description = 'Soapee Community Soap Recipes';
96 |
97 | return [
98 | {name: 'description', content: description},
99 | {name: 'twitter:card', content: description},
100 | {name: 'twitter:title', content: description},
101 | {property: 'og:title', content: description}
102 | ];
103 | }
104 |
105 |
106 | } );
--------------------------------------------------------------------------------
/src/app/components/listOilsSelector.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react/addons';
3 | import Reflux from 'reflux';
4 | import cx from 'classnames';
5 |
6 | import oilsStore from 'stores/oils';
7 |
8 | export default React.createClass( {
9 |
10 | mixins: [
11 | Reflux.connect( oilsStore, 'oils' ),
12 | React.addons.LinkedStateMixin
13 | ],
14 |
15 | shouldComponentUpdate( nextProps, nextState ) {
16 | return !( _.isEqual( this.state, nextState ) );
17 | },
18 |
19 | render() {
20 | return (
21 |
22 |
23 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | { this.renderOils() }
33 |
34 |
35 |
36 |
37 | );
38 | },
39 |
40 | renderOils() {
41 | let oils;
42 |
43 | function matchToName( oil ) {
44 | return oil.name.toLowerCase().indexOf( this.state.filter.toLowerCase() ) !== -1;
45 | }
46 |
47 | function renderOil( oil ) {
48 | let klass = cx( 'no-select', {selected: oil.name === this.state.selectedOil} );
49 |
50 | return (
51 |
52 | |
56 | {oil.name}
57 | |
58 |
59 | );
60 | }
61 |
62 | if ( this.state.filter ) {
63 | oils = _( this.state.oils )
64 | .filter( matchToName, this );
65 |
66 | } else {
67 | oils = _( this.state.oils );
68 | }
69 |
70 | return oils
71 | .sortBy( 'name' )
72 | .map( renderOil, this )
73 | .value();
74 | },
75 |
76 | selectOil( oil ) {
77 | return () => {
78 | this.setState( {
79 | selectedOil: oil.name
80 | } );
81 |
82 | if ( this.props.onSelectedOil ) {
83 | this.props.onSelectedOil( oil );
84 | }
85 | };
86 | },
87 |
88 | addOil( oil ) {
89 | return e => {
90 | this.setState( {
91 | selectedOil: oil.name
92 | } );
93 |
94 | if ( this.props.onAddedOil ) {
95 | this.props.onAddedOil( e, oil );
96 | }
97 | };
98 | },
99 |
100 | clearSearch() {
101 | this.setState( {
102 | filter: null
103 | } );
104 | }
105 |
106 | } );
--------------------------------------------------------------------------------
/src/app/components/propertiesOil.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 |
4 | import oilsStore from 'stores/oils';
5 | import calculatorStore from 'stores/calculator';
6 |
7 | export default React.createClass( {
8 |
9 | shouldComponentUpdate( nextProps ) {
10 | return !( _.isEqual( this.props, nextProps ) );
11 | },
12 |
13 | render() {
14 | let oil = this.props.oil;
15 |
16 | return (
17 |
18 |
19 | { oil &&
20 |
21 |
22 |
23 |
24 | Sap KOH NaOH |
25 | {oil.sap} {calculatorStore.sapForNaOh(oil)} |
26 |
27 |
28 | | Iodine |
29 | {oil.iodine} |
30 |
31 |
32 | | INS |
33 | {oil.ins} |
34 |
35 | {this.gap()}
36 | {this.renderFats()}
37 | {this.gap()}
38 | {this.renderProperties()}
39 | {this.gap()}
40 | {this.renderSaturation()}
41 |
42 |
43 |
44 | }
45 |
46 | );
47 | },
48 |
49 | gap() {
50 | return (
51 |
52 | |
53 |
54 | );
55 | },
56 |
57 | renderFats() {
58 | let oil = this.props.oil;
59 |
60 | return _.transform( oilsStore.getAllFats(), ( output, fat ) => {
61 | let breakdown = oil.breakdown[ fat ];
62 |
63 | if ( breakdown ) {
64 | output.push(
65 |
66 | | {_.capitalize(fat)} |
67 | {breakdown}% |
68 |
69 | );
70 | }
71 | }, [] );
72 | },
73 |
74 | renderProperties() {
75 | let oil = this.props.oil;
76 |
77 | function render( property ) {
78 | return (
79 |
80 | | {_.capitalize( property )} |
81 | {oil.properties[ property ]}% |
82 |
83 | );
84 | }
85 |
86 | return _( oil.properties )
87 | .keys()
88 | .sort()
89 | .map( render, this )
90 | .value();
91 | },
92 |
93 | renderSaturation() {
94 | let oil = this.props.oil;
95 |
96 | return _.map( oil.saturations, ( satType, saturation ) => {
97 | return (
98 |
99 | | {_.capitalize(saturation)}: |
100 | {satType}% |
101 |
102 | );
103 | } );
104 | }
105 |
106 | } );
--------------------------------------------------------------------------------
/src/app/stores/oils.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import Reflux from 'reflux';
3 |
4 | import { getOils } from 'resources/oils';
5 | import Oil from 'models/oil';
6 |
7 | export default Reflux.createStore( {
8 |
9 | store: [],
10 |
11 | init() {
12 | this.loadOils();
13 | },
14 |
15 | getInitialState() {
16 | return this.store;
17 | },
18 |
19 | getAllFats() {
20 | return [
21 | 'capric', 'caprylic', 'docosadienoic', 'docosenoid', 'eicosenoic', 'erucic', 'lauric', 'linoleic', 'linolenic', 'myristic', 'oleic', 'palmitic', 'ricinoleic', 'stearic'
22 | ];
23 | },
24 |
25 | getOilById( oilId ) {
26 | return _.find( this.store, { id: oilId } );
27 | },
28 |
29 | getFlatOilProperties() {
30 | return _.map( this.store, oil => {
31 | return {
32 | id: oil.id,
33 | name: oil.name,
34 | sap: oil.sap,
35 | iodine: oil.iodione,
36 | ins: oil.ins,
37 |
38 | bubbly: oil.properties.bubbly,
39 | cleansing: oil.properties.cleansing,
40 | condition: oil.properties.condition,
41 | hardness: oil.properties.hardness,
42 | longevity: oil.properties.longevity,
43 | stability: oil.properties.stable,
44 |
45 | capric: oil.breakdown.capric || 0,
46 | caprylic: oil.breakdown.caprylic || 0,
47 | docosadienoic: oil.breakdown.docosadienoic || 0,
48 | docosenoid: oil.breakdown.docosenoid || 0,
49 | eicosenoic: oil.breakdown.eicosenoic || 0,
50 | erucic: oil.breakdown.erucic || 0,
51 | lauric: oil.breakdown.lauric || 0,
52 | linoleic: oil.breakdown.linoleic || 0,
53 | linolenic: oil.breakdown.linolenic || 0,
54 | myristic: oil.breakdown.myristic || 0,
55 | oleic: oil.breakdown.oleic || 0,
56 | palmitic: oil.breakdown.palmitic || 0,
57 | ricinoleic: oil.breakdown.ricinoleic || 0,
58 | stearic: oil.breakdown.stearic || 0,
59 |
60 | saturated: oil.saturation.saturated,
61 | monoSaturated: oil.saturation.monoSaturated,
62 | polySaturated: oil.saturation.polySaturated
63 | };
64 | } );
65 | },
66 |
67 | oilPropertyGroupings() {
68 | return {
69 | 'fats-common': [ 'name', 'sap', 'lauric', 'linoleic', 'linolenic', 'myristic', 'oleic', 'palmitic', 'ricinoleic', 'stearic' ],
70 | 'fats-all': [ 'name', 'sap', 'capric', 'caprylic', 'docosadienoic', 'docosenoid', 'eicosenoic', 'erucic', 'lauric', 'linoleic', 'linolenic', 'myristic', 'oleic', 'palmitic', 'ricinoleic', 'stearic' ],
71 | properties: [ 'name', 'sap', 'bubbly', 'cleansing', 'condition', 'hardness', 'longevity', 'stability' ],
72 | saturation: [ 'name', 'sap', 'saturated', 'monoSaturated', 'polySaturated' ]
73 | };
74 |
75 | },
76 |
77 | loadOils() {
78 | function assignToStore( data ) {
79 | this.store = _.map( data, oil => (new Oil( oil )).getExtendedOil() );
80 | }
81 |
82 | function doTrigger() {
83 | this.trigger( this.store );
84 | }
85 |
86 | getOils()
87 | .then( assignToStore.bind( this ) )
88 | .then( doTrigger.bind( this ) );
89 | }
90 |
91 | } );
--------------------------------------------------------------------------------
/src/app/components/addComment.js:
--------------------------------------------------------------------------------
1 | import React from 'react/addons';
2 | import Reflux from 'reflux';
3 |
4 | import authStore from 'stores/auth';
5 | import ValidateComment from 'services/validateComment';
6 |
7 | import MarkdownEditor from 'components/markdownEditor';
8 | import BootstrapModalLink from 'components/bootstrapModalLink';
9 |
10 | import SignupOrLoginToSaveRecipe from 'modals/signupOrLoginToSaveRecipe';
11 |
12 | export default React.createClass( {
13 |
14 | mixins: [
15 | React.addons.LinkedStateMixin,
16 | Reflux.connect( authStore, 'auth' )
17 | ],
18 |
19 | getDefaultProps() {
20 | return {
21 | onNewComment: () => {}
22 | };
23 | },
24 |
25 | getInitialState() {
26 | return {
27 | comments: null
28 | };
29 | },
30 |
31 | render() {
32 | return (
33 |
34 |
39 |
40 | { this.state.errors &&
41 |
42 | { this.state.errors }
43 |
44 | }
45 |
46 |
47 | { this.renderAddCommentButton() }
48 |
49 |
50 | );
51 | },
52 |
53 | renderAddCommentButton() {
54 | let button;
55 | let caption;
56 |
57 | caption =
Add Comment;
58 |
59 | if ( authStore.isAuthenticated() ) {
60 | button =
;
61 | } else {
62 | button = (
63 |
{ caption }}
65 | modal={SignupOrLoginToSaveRecipe}
66 | action="post comments"
67 | />
68 | );
69 | }
70 |
71 | return button;
72 | },
73 |
74 | addComment() {
75 |
76 | function validate() {
77 | return new ValidateComment( {
78 | comment: this.state.comments
79 | } )
80 | .execute();
81 | }
82 |
83 | function triggerAddComment() {
84 | this.props.onNewComment( this.state.comments );
85 | }
86 |
87 | function clear() {
88 | this.setState( { comments: '' } );
89 | }
90 |
91 |
92 | this.setState( {
93 | errors: null
94 | } );
95 |
96 | validate.call( this )
97 | .with( this )
98 | .then( triggerAddComment )
99 | .then( clear )
100 | .catch( this.showError );
101 | },
102 |
103 | showError( e ) {
104 | if ( e.name === 'CheckitError' ) {
105 | this.setState( {
106 | errors: e.toJSON()
107 | } );
108 | } else {
109 | this.setState( {
110 | errors: 'Ooops. Something went wrong!!1'
111 | } );
112 | }
113 | }
114 |
115 | } );
--------------------------------------------------------------------------------
/src/app/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Route, DefaultRoute} from 'react-router';
3 |
4 | import Login from 'views/login';
5 | import Signup from 'views/signup';
6 | import Forgot from 'views/forgot';
7 |
8 | import Account from 'views/account';
9 | import Application from 'views/application';
10 | import Calculator from 'views/calculator';
11 | import Logout from 'views/logout';
12 | import Feed from 'views/feed';
13 | import MainLanding from 'views/mainLanding';
14 | import Oil from 'views/oil';
15 | import Oils from 'views/oils';
16 | import PrintCalculation from 'views/printCalculation';
17 | import Recipe from 'views/recipe';
18 | import RecipeEdit from 'views/recipeEdit';
19 | import RecipePrint from 'views/recipePrint';
20 | import Recipes from 'views/recipes';
21 | import RecipeJournal from 'views/recipeJournal';
22 | import RecipeJournalEdit from 'views/recipeJournalEdit';
23 | import MyFriendsRecipes from 'views/myFriendsRecipes';
24 | import MyStatusUpdates from 'views/myStatusUpdates';
25 | import Resources from 'views/resources';
26 | import StatusUpdate from 'views/statusUpdate';
27 | import StatusUpdateEdit from 'views/statusUpdateEdit';
28 | import UserProfile from 'views/userProfile';
29 |
30 | import MyProfile from 'views/myProfile';
31 | import MyComments from 'views/myComments';
32 | import MyRecipes from 'views/myRecipes';
33 | import SavedRecipes from 'views/savedRecipes';
34 |
35 |
36 | let routes = (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | );
81 |
82 | export default routes;
--------------------------------------------------------------------------------
/src/app/components/recipeJournalItem.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import moment from 'moment';
3 | import React from 'react';
4 | import { Link } from 'react-router';
5 |
6 | import authStore from 'stores/auth';
7 |
8 | import ImageableThumbnails from 'components/imageableThumbnails';
9 | import ImageableCarousel from 'components/imageableCarousel';
10 | import MarkedDisplay from 'components/markedDisplay';
11 | import UserAvatar from 'components/userAvatar';
12 |
13 | export default React.createClass( {
14 |
15 | getDefaultProps() {
16 | return {
17 | imagesType: 'thumbnails',
18 | state: 'list'
19 | };
20 | },
21 |
22 | render() {
23 | let recipeJournal = this.props.recipeJournal;
24 | let recipe = this.props.recipe;
25 |
26 | let paramLink = {
27 | recipeId: recipe.id,
28 | journalId: recipeJournal.id
29 | };
30 |
31 | return (
32 |
33 |
34 |
37 |
38 |
39 |
40 |
41 | { recipe.user.name }
42 |
43 |
46 | { moment( recipeJournal.created_at ).fromNow() }
47 |
48 |
49 |
50 |
53 |
54 | { this.props.imagesType === 'thumbnails' && _.get( recipeJournal, 'images.length' ) > 0 &&
55 |
58 | }
59 |
60 | { this.props.imagesType === 'carousel' && _.get( recipeJournal, 'images.length' ) > 0 &&
61 |
62 |
63 |
64 |
65 |
68 |
69 |
70 |
71 | }
72 |
73 |
74 |
75 | { this.props.state === 'list' && View }
76 | { authStore.isMyId( recipe.user_id ) && Edit }
77 |
78 |
79 |
80 | );
81 | }
82 |
83 | } );
--------------------------------------------------------------------------------
/src/app/components/localAuthenticationForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react/addons';
2 | import cx from 'classnames';
3 |
4 | export default React.createClass( {
5 |
6 | mixins: [
7 | React.addons.LinkedStateMixin
8 | ],
9 |
10 | getInitialState() {
11 | return {
12 | username: null,
13 | password: null,
14 | email: null
15 | };
16 | },
17 |
18 | render() {
19 | let passwordFieldType;
20 |
21 | passwordFieldType = this.props.hidePassword ? 'password' : 'text';
22 |
23 | return (
24 |
67 | );
68 | },
69 |
70 | formClassNames( field ) {
71 | return cx( 'form-group', {
72 | 'has-error': this.props.errors[ field ],
73 | 'has-success': this.state[ field ] && !(this.props.errors[ field ])
74 | } );
75 | },
76 |
77 | signup( e ) {
78 | e.preventDefault();
79 |
80 | this.props.onButtonClick( {
81 | username: this.state.username,
82 | password: this.state.password,
83 | email: this.state.email
84 | } );
85 | }
86 |
87 | } );
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var path = require( 'path' );
2 | var gulp = require( 'gulp' );
3 | var gutil = require( 'gulp-util' );
4 | var webpack = require( 'webpack' );
5 | var gulpWebpack = require( 'gulp-webpack' );
6 | var WebpackDevServer = require( 'webpack-dev-server' );
7 | var stylus = require( 'gulp-stylus' );
8 | var clean = require( 'gulp-clean' );
9 | var runSequence = require( 'run-sequence' );
10 | var imagemin = require( 'gulp-imagemin' );
11 | var shipitCaptain = require('shipit-captain');
12 |
13 | function handleError( task ) {
14 | return function ( err ) {
15 | this.emit( 'end' );
16 | gutil.log( 'Error handler for', task, err.toString() );
17 | };
18 | }
19 |
20 | // The development server (the recommended option for development)
21 | gulp.task( 'default', [ 'webpack-dev-server', 'stylus:compile' ] );
22 |
23 | gulp.task( 'webpack-dev-server', function ( callback ) {
24 | var config = Object.create( require( './webpack.dev.js' ) );
25 |
26 | // Start a webpack-dev-server
27 | new WebpackDevServer( webpack( config ), {
28 | contentBase: path.join( __dirname, 'src' ),
29 | publicPath: config.output.publicPath,
30 | hot: true,
31 | historyApiFallback: true,
32 | stats: {
33 | colors: true
34 | }
35 | } ).listen( 8080, '0.0.0.0', function ( err ) {
36 | if ( err ) {
37 | throw new gutil.PluginError( 'webpack-dev-server', err );
38 | }
39 | gutil.log( '[webpack-dev-server]', 'http://192.168.30.20:8080' );
40 | callback();
41 | } );
42 |
43 | //setup stylus watcher
44 | gulp.watch( [ 'src/assets/stylus/*.styl', 'src/assets/stylus/**/*.styl' ], [ 'stylus:compile' ] );
45 | } );
46 |
47 | gulp.task( 'stylus:compile', function () {
48 | return gulp.src( './src/assets/stylus/main.styl' )
49 | .pipe( stylus().on( 'error', handleError( 'stylus:compile' ) ) )
50 | .pipe( gulp.dest( './src/assets' ) );
51 | } );
52 |
53 | gulp.task( 'clean:build', function () {
54 | return gulp.src( 'build/*', { read: false } )
55 | .pipe( clean() );
56 | } );
57 |
58 | gulp.task( 'build:image:min', function () {
59 | return gulp.src( './build/bundle/*.jpg' )
60 | .pipe( imagemin( {
61 | progressive: true,
62 | svgoPlugins: [ { removeViewBox: false } ]
63 | } ) )
64 | .pipe( gulp.dest( 'build/bundle' ) );
65 | } );
66 |
67 | gulp.task( 'build:cp:index', function () {
68 | return gulp.src( [
69 | './src/index.html',
70 | './src/favicon.png',
71 | './src/assets/img/logo.jpg'
72 | ] )
73 | .pipe( gulp.dest( 'build/' ) );
74 | } );
75 |
76 | gulp.task( 'build:webpack', function () {
77 | return gulp.src( 'src/app/app.js' )
78 | .pipe( gulpWebpack( require( './webpack.prod.js' ), webpack ) )
79 | .pipe( gulp.dest( 'build/bundle/' ) );
80 | } );
81 |
82 |
83 | gulp.task( 'build', function ( cb ) {
84 | runSequence(
85 | 'clean:build',
86 | [ 'stylus:compile', 'build:cp:index' ],
87 | 'build:webpack',
88 | 'build:image:min',
89 | cb
90 | );
91 | } );
92 |
93 | gulp.task( 'deploy', [ 'build' ], function ( cb ) {
94 | var options = {
95 | init: require( './deploy/shipit' ).init,
96 | run: 'deploy-local',
97 | targetEnv: 'production',
98 | confirm: false
99 | };
100 |
101 | shipitCaptain( require( './deploy/shipit' ).config, options, cb );
102 | } );
--------------------------------------------------------------------------------
/src/app/views/calculator.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Reflux from 'reflux';
3 | import DocMeta from 'react-doc-meta';
4 |
5 | import { Navigation } from 'react-router';
6 |
7 | import calculatorStore from 'stores/calculator';
8 | import recipeActions from 'actions/recipe';
9 |
10 | import FormSaveRecipe from 'components/formSaveRecipe';
11 | import Imageable from 'components/imageable';
12 | import SapCalculator from 'components/sapCalculator';
13 |
14 | export default React.createClass( {
15 |
16 | mixins: [
17 | Navigation,
18 | Reflux.connect( calculatorStore, 'recipe' )
19 | ],
20 |
21 | getInitialState() {
22 | return {
23 | saving: false
24 | };
25 | },
26 |
27 | componentDidMount() {
28 | document.title = 'Soapee - Lye Calculator';
29 | },
30 |
31 | render() {
32 | return (
33 |
34 |
35 |
38 |
39 | { this.state.recipe.countWeights() > 0 &&
40 |
41 |
52 |
53 | }
54 |
55 |
56 | );
57 | },
58 |
59 | startImageUploadHookFn( fnToStartUploads ) {
60 | this.startUploads = fnToStartUploads;
61 | },
62 |
63 | saveCaption() {
64 | return this.state.saving ? 'Saving Recipe' : 'Save Recipe';
65 | },
66 |
67 | saveRecipe() {
68 | function uploadImages() {
69 | this.startUploads( this.newRecipe.id );
70 | }
71 |
72 | this.setState( {
73 | saving: true
74 | } );
75 |
76 | return recipeActions.createRecipe( this.state.recipe )
77 | .then( recipe => this.newRecipe = recipe )
78 | .then( this.showRecipe.bind(this) )
79 | .finally(() => this.setState({
80 | saving: false
81 | }));
82 | },
83 |
84 | showRecipe() {
85 | this.resetRecipe();
86 | this.transitionTo( 'recipe', { id: this.newRecipe.id } );
87 | },
88 |
89 | printRecipe() {
90 | this.replaceWith( 'print' );
91 | },
92 |
93 | printPreviewRecipe() {
94 | this.transitionTo( 'print', null, { preview: true } );
95 | },
96 |
97 | resetRecipe() {
98 | recipeActions.resetRecipe();
99 | },
100 |
101 | tags() {
102 | let description = 'Soapee Lye Calculator';
103 |
104 | return [
105 | {name: 'description', content: description},
106 | {name: 'twitter:card', content: description},
107 | {name: 'twitter:title', content: description},
108 | {property: 'og:title', content: description}
109 | ];
110 | }
111 |
112 | } );
--------------------------------------------------------------------------------
/src/app/components/recipePrintArea.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import RecipeBreakdown from 'components/recipeBreakdown';
4 | import RecipeTotals from 'components/recipeTotals';
5 | import RecipeFattyAcids from 'components/recipeFattyAcids';
6 | import RecipeProperties from 'components/recipeProperties';
7 | import MarkedDisplay from 'components/markedDisplay';
8 |
9 | export default React.createClass( {
10 |
11 | render() {
12 | let recipeName = this.props.recipe.getModelValue( 'name' );
13 | let recipeDescription = this.props.recipe.getModelValue( 'description');
14 | let recipeNotes = this.props.recipe.getModelValue( 'notes');
15 |
16 | return (
17 |
18 |
19 |
20 |
21 | { recipeName &&
22 |
23 |
24 |
25 |
{ recipeName }
26 |
27 |
28 |
29 | }
30 |
31 | { recipeDescription &&
32 |
39 | }
40 |
41 |
42 |
43 |
Recipe Oils
44 |
47 |
48 |
49 |
Recipe Totals
50 |
53 |
54 |
55 |
56 |
Summaries
57 |
58 |
59 |
63 |
64 |
65 |
Fatty Acids %
66 |
69 |
70 |
71 |
72 | { recipeNotes &&
73 |
74 |
75 |
Notes
76 |
77 |
78 |
79 |
80 |
81 | }
82 |
83 |
84 |
85 |
86 | );
87 | }
88 |
89 |
90 | } );
--------------------------------------------------------------------------------
/src/app/views/recipeEdit.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import React from 'react';
3 | import Reflux from 'reflux';
4 | import { Navigation } from 'react-router';
5 |
6 | import recipeActions from 'actions/recipe';
7 | import recipeStore from 'stores/recipe';
8 |
9 | import SapCalculator from 'components/sapCalculator';
10 | import FormSaveRecipe from 'components/formSaveRecipe';
11 | import Imageable from 'components/imageable';
12 | import ImageableEdit from 'components/imageableEdit';
13 |
14 | export default React.createClass( {
15 |
16 | statics: {
17 | willTransitionTo: function ( transition, params ) {
18 | recipeActions.getRecipeById( params.id );
19 | }
20 | },
21 |
22 | mixins: [
23 | Navigation,
24 | Reflux.connect( recipeStore, 'recipe' )
25 | ],
26 |
27 | render() {
28 | document.title = 'Soapee - Edit';
29 |
30 | return (
31 |
32 |
35 |
36 | { _.get(this.state, 'recipe.recipe.images.length' ) > 0 &&
37 |
38 |
39 |
40 |
43 |
44 |
45 | }
46 |
47 | { this.state.recipe.countWeights() > 0 &&
48 |
49 |
58 |
59 | }
60 |
61 |
62 | );
63 | },
64 |
65 | startImageUploadHookFn( fnToStartUploads ) {
66 | this.startUploads = fnToStartUploads;
67 | },
68 |
69 | saveCaption() {
70 | return this.state.saving ? 'Saving Recipe' : 'Save Recipe';
71 | },
72 |
73 | saveRecipe() {
74 | return this.doSaveAction( recipeActions.updateRecipe );
75 | },
76 |
77 | saveAsRecipe() {
78 | return this.doSaveAction( recipeActions.createRecipe );
79 | },
80 |
81 | doSaveAction( action ) {
82 | this.setState( {
83 | saving: true
84 | } );
85 |
86 | recipeStore.calculate();
87 |
88 | return action( this.state.recipe )
89 | .then( this.toRecipeView.bind( this ) )
90 | .finally(() => this.setState({
91 | saving: false
92 | }));
93 |
94 | function uploadImages() {
95 | this.startUploads( this.state.recipe.getModelValue( 'id' ) );
96 | }
97 | },
98 |
99 | printRecipe() {
100 | this.replaceWith( 'printRecipe', { id: this.state.recipe.getModelValue( 'id' ) } );
101 | },
102 |
103 | goBackToView() {
104 | this.toRecipeView( this.state.recipe.recipe );
105 | },
106 |
107 | toRecipeView(recipe) {
108 | this.transitionTo( 'recipe', { id: recipe.id} );
109 | }
110 |
111 | } );
--------------------------------------------------------------------------------