├── .eslintignore ├── src ├── assets │ ├── stylus │ │ ├── variables.styl │ │ ├── views │ │ │ ├── calculator.styl │ │ │ ├── oil.styl │ │ │ ├── forgot.styl │ │ │ ├── status-update-edit.styl │ │ │ ├── recipe-journal-edit.styl │ │ │ ├── recipes.styl │ │ │ ├── status-update.styl │ │ │ ├── profile.styl │ │ │ ├── my-status-updates.styl │ │ │ ├── user-profile.styl │ │ │ ├── recipe.styl │ │ │ ├── application.styl │ │ │ └── main-landing.styl │ │ ├── components │ │ │ ├── bootstrap-modal.styl │ │ │ ├── properties-oil.styl │ │ │ ├── spinner.styl │ │ │ ├── imageable.styl │ │ │ ├── tooltip-question.styl │ │ │ ├── imageable-thumbnails.styl │ │ │ ├── google-comments.styl │ │ │ ├── add-comment.styl │ │ │ ├── recipe-journal-add.styl │ │ │ ├── text-editor.styl │ │ │ ├── facebook-comments.styl │ │ │ ├── comment.styl │ │ │ ├── list-oils-recipe.styl │ │ │ ├── recipe-fatty-acids.styl │ │ │ ├── share-this.styl │ │ │ ├── form-save-recipe.styl │ │ │ ├── list-oils-selector.styl │ │ │ ├── markdown-editor.styl │ │ │ ├── animated-modal.styl │ │ │ ├── feed-item.styl │ │ │ ├── imageable-edit.styl │ │ │ ├── recipe-list-item.styl │ │ │ ├── recipe-journal-item.styl │ │ │ ├── user-avatar.styl │ │ │ ├── navbar-component.styl │ │ │ ├── button-user-notifications.styl │ │ │ ├── media-signin-buttons.styl │ │ │ ├── footer.styl │ │ │ ├── recipe-print-area.styl │ │ │ ├── user-notifications.styl │ │ │ ├── sap-calculator.styl │ │ │ └── grid-oil.styl │ │ ├── modals │ │ │ ├── select-oils.styl │ │ │ └── signup-or-login-to-save-changes.styl │ │ ├── mixins.styl │ │ ├── global.styl │ │ ├── transitions.styl │ │ ├── overrides.styl │ │ └── main.styl │ ├── img │ │ ├── logo.jpg │ │ └── landing-background.jpg │ └── css │ │ └── filepicker.css ├── favicon.png ├── app │ ├── utils │ │ ├── baseUrl.js │ │ ├── roundFormated.js │ │ └── http.js │ ├── actions │ │ ├── feed.js │ │ ├── user.js │ │ ├── oil.js │ │ ├── reset.js │ │ ├── auth.js │ │ ├── statusUpdate.js │ │ ├── me.js │ │ └── recipe.js │ ├── components │ │ ├── spinner.js │ │ ├── navLink.js │ │ ├── imageableThumbnails.js │ │ ├── markedDisplay.js │ │ ├── portalSignup.js │ │ ├── tooltipQuestion.js │ │ ├── facebookComments.js │ │ ├── recipesLinkTable.js │ │ ├── markdownEditor.js │ │ ├── imageableCarousel.js │ │ ├── animatedModal.js │ │ ├── bootstrapModalLink.js │ │ ├── recipeFattyAcids.js │ │ ├── buttonGPlusLike.js │ │ ├── buttonFBLike.js │ │ ├── footer.js │ │ ├── recipeJournals.js │ │ ├── imageableEdit.js │ │ ├── commentable.js │ │ ├── userAvatar.js │ │ ├── googleComments.js │ │ ├── shareThis.js │ │ ├── buttonUserNotifications.js │ │ ├── comment.js │ │ ├── localSignupForm.js │ │ ├── imageable.js │ │ ├── griddlePager.js │ │ ├── localLoginForm.js │ │ ├── listOilsSelector.js │ │ ├── propertiesOil.js │ │ ├── addComment.js │ │ ├── recipeJournalItem.js │ │ ├── localAuthenticationForm.js │ │ └── recipePrintArea.js │ ├── services │ │ ├── validateEmail.js │ │ ├── validateComment.js │ │ ├── validateRecipeFormFields.js │ │ ├── validateProfileForm.js │ │ ├── validatePassword.js │ │ ├── validateRequiredField.js │ │ ├── validateLoginFields.js │ │ └── validateSignupFields.js │ ├── resources │ │ ├── feed.js │ │ ├── imageable.js │ │ ├── users.js │ │ ├── oils.js │ │ ├── resets.js │ │ ├── authFacebook.js │ │ ├── statusUpdates.js │ │ ├── auths.js │ │ ├── authGoogle.js │ │ └── me.js │ ├── views │ │ ├── logout.js │ │ ├── account.js │ │ ├── application.js │ │ ├── oils.js │ │ ├── printCalculation.js │ │ ├── recipePrint.js │ │ ├── signup.js │ │ ├── myFriendsRecipes.js │ │ ├── login.js │ │ ├── myRecipes.js │ │ ├── savedRecipes.js │ │ ├── feed.js │ │ ├── myComments.js │ │ ├── recipeJournal.js │ │ ├── recipes.js │ │ ├── calculator.js │ │ └── recipeEdit.js │ ├── stores │ │ ├── myComments.js │ │ ├── userProfile.js │ │ ├── oil.js │ │ ├── statusUpdate.js │ │ ├── myFriendsRecipes.js │ │ ├── userRecipes.js │ │ ├── calculator.js │ │ ├── myStatusUpdates.js │ │ ├── myFriends.js │ │ ├── recipes.js │ │ ├── feed.js │ │ ├── userFriends.js │ │ ├── recipeJournal.js │ │ ├── analytics.js │ │ ├── recipeJournals.js │ │ ├── oilComments.js │ │ ├── recipeComments.js │ │ ├── userNotifications.js │ │ ├── statusUpdateComments.js │ │ ├── journalComments.js │ │ ├── recipe.js │ │ ├── me.js │ │ ├── auth.js │ │ └── oils.js │ ├── app.js │ ├── config.js │ ├── models │ │ └── oil.js │ ├── mixins │ │ └── formLinkHandlers.js │ ├── lib │ │ └── growl.js │ └── routes.js └── index.html ├── shipitfile.js ├── .gitignore ├── provision ├── client.sh └── base.sh ├── deploy ├── deploy.js └── shipit.js ├── .editorconfig ├── README.md ├── webpack.dev.js ├── Vagrantfile ├── webpack.prod.js ├── LICENSE ├── .eslintrc ├── package.json └── gulpfile.js /.eslintignore: -------------------------------------------------------------------------------- 1 | **/src/app/lib/*.js -------------------------------------------------------------------------------- /src/assets/stylus/variables.styl: -------------------------------------------------------------------------------- 1 | footer-height = 60px; -------------------------------------------------------------------------------- /shipitfile.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./deploy/shipit').init; -------------------------------------------------------------------------------- /src/assets/stylus/views/calculator.styl: -------------------------------------------------------------------------------- 1 | #calculator { 2 | 3 | } -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nazar/soapee-ui/HEAD/src/favicon.png -------------------------------------------------------------------------------- /src/assets/img/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nazar/soapee-ui/HEAD/src/assets/img/logo.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vagrant 3 | 4 | 5 | npm-debug.log* 6 | node_modules 7 | build 8 | 9 | deploy/keys -------------------------------------------------------------------------------- /src/assets/img/landing-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nazar/soapee-ui/HEAD/src/assets/img/landing-background.jpg -------------------------------------------------------------------------------- /src/assets/stylus/components/bootstrap-modal.styl: -------------------------------------------------------------------------------- 1 | .bootstrap-modal { 2 | 3 | .modal { 4 | z-index: 10000; 5 | } 6 | 7 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/properties-oil.styl: -------------------------------------------------------------------------------- 1 | .properties-oil { 2 | 3 | .title { 4 | font-style:italic; 5 | } 6 | 7 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/spinner.styl: -------------------------------------------------------------------------------- 1 | .spinner { 2 | text-align: center; 3 | 4 | .fa { 5 | font-size: 30px; 6 | } 7 | } -------------------------------------------------------------------------------- /provision/client.sh: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env bash 2 | 3 | # fix putty ctrl+left right arrow navigation 4 | 5 | echo 'tput smkx' >> /home/vagrant/.profile 6 | -------------------------------------------------------------------------------- /src/assets/stylus/components/imageable.styl: -------------------------------------------------------------------------------- 1 | .imageable { 2 | 3 | .dz-image-preview { 4 | background: transparent !important; 5 | } 6 | 7 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/tooltip-question.styl: -------------------------------------------------------------------------------- 1 | .tooltip-question { 2 | color: #2196f3 - 30%; 3 | margin-left: 5px; 4 | 5 | font-size: 16px; 6 | } -------------------------------------------------------------------------------- /src/assets/stylus/modals/select-oils.styl: -------------------------------------------------------------------------------- 1 | .select-oils { 2 | 3 | td.selected { 4 | padding-left: 6px; 5 | padding-right: 0; 6 | } 7 | } -------------------------------------------------------------------------------- /src/app/utils/baseUrl.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import {resolve} from 'url'; 3 | 4 | export default function( url ) { 5 | return resolve( config.baseUrl, url ); 6 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/imageable-thumbnails.styl: -------------------------------------------------------------------------------- 1 | .imageable-thumbnails { 2 | margin-top: 10px; 3 | 4 | img.thumbnail { 5 | margin-left: 10px; 6 | } 7 | } -------------------------------------------------------------------------------- /src/assets/stylus/views/oil.styl: -------------------------------------------------------------------------------- 1 | #oil { 2 | 3 | h1 { 4 | font-size: 30px; 5 | } 6 | 7 | .fb-like { 8 | margin-bottom: 20px; 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/google-comments.styl: -------------------------------------------------------------------------------- 1 | .google-comments { 2 | 3 | width: 100% !important; 4 | 5 | & > iframe { 6 | width: 100% !important; 7 | } 8 | 9 | 10 | } -------------------------------------------------------------------------------- /src/assets/stylus/views/forgot.styl: -------------------------------------------------------------------------------- 1 | #forgot { 2 | 3 | label { 4 | font-weight: bold; 5 | margin-right: 5px; 6 | } 7 | 8 | .spacer { 9 | margin-top 15px; 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/add-comment.styl: -------------------------------------------------------------------------------- 1 | .add-comment { 2 | 3 | textarea { 4 | width: 50%; 5 | } 6 | 7 | .alert { 8 | padding: 2px 3px; 9 | margin: 0 0 15px 0; 10 | width: 50%; 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/assets/stylus/views/status-update-edit.styl: -------------------------------------------------------------------------------- 1 | #status-update-edit { 2 | 3 | .markdown-editor { 4 | textarea { 5 | width: 50%; 6 | } 7 | } 8 | 9 | .dropzone { 10 | margin-bottom: 20px; 11 | } 12 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/recipe-journal-add.styl: -------------------------------------------------------------------------------- 1 | .recipe-journal-add { 2 | 3 | .markdown-editor { 4 | textarea { 5 | width: 50%; 6 | } 7 | } 8 | 9 | .dropzone { 10 | margin-bottom: 20px; 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/assets/stylus/views/recipe-journal-edit.styl: -------------------------------------------------------------------------------- 1 | #recipe-journal-edit { 2 | 3 | .markdown-editor { 4 | textarea { 5 | width: 50%; 6 | } 7 | } 8 | 9 | .dropzone { 10 | margin-bottom: 20px; 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/text-editor.styl: -------------------------------------------------------------------------------- 1 | .text-editor { 2 | 3 | .btn-toolbar { 4 | padding-bottom: 3px; 5 | } 6 | 7 | .editor { 8 | border: 1px solid #e5e5e5; 9 | border-radius: 3px; 10 | 11 | padding: 5px; 12 | } 13 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/facebook-comments.styl: -------------------------------------------------------------------------------- 1 | .facebook-comments { 2 | 3 | .fb_iframe_widget, 4 | .fb_iframe_widget span, 5 | .fb_iframe_widget span iframe[style] { 6 | min-width: 100% !important; 7 | width: 100% !important; 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /src/app/actions/feed.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | import { 4 | getFeed 5 | } from 'resources/feed'; 6 | 7 | let actions = Reflux.createActions( { 8 | getFeed: { asyncResult: true } 9 | } ); 10 | 11 | export default actions; 12 | 13 | actions.getFeed.listenAndPromise( getFeed ); -------------------------------------------------------------------------------- /src/assets/stylus/modals/signup-or-login-to-save-changes.styl: -------------------------------------------------------------------------------- 1 | .signup-or-login-to-save-changes { 2 | 3 | .alert { 4 | margin: 0; 5 | margin-bottom: 30px; 6 | } 7 | 8 | .strike { 9 | .or { 10 | background-color: white; 11 | } 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/comment.styl: -------------------------------------------------------------------------------- 1 | .comment { 2 | 3 | margin-top: 10px; 4 | margin-bottom: 10px; 5 | 6 | padding: 3px; 7 | border: 1px solid silver + 50%; 8 | border-radius: 3px; 9 | 10 | box-shadow: 0 1px 4px rgba(0,0,0,0.1); 11 | 12 | media-common-styles(); 13 | 14 | } -------------------------------------------------------------------------------- /src/app/components/spinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.createClass( { 4 | 5 | render() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | 13 | } ); -------------------------------------------------------------------------------- /src/assets/stylus/components/list-oils-recipe.styl: -------------------------------------------------------------------------------- 1 | .list-oils-recipe { 2 | 3 | td.value { 4 | min-width: 83px; 5 | 6 | .short-numeric { 7 | width: 50px !important; 8 | } 9 | } 10 | 11 | .unit { 12 | color: #aeaeae; 13 | padding-left: 5px; 14 | } 15 | } -------------------------------------------------------------------------------- /src/app/utils/roundFormated.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export default function roundFormatted( weight, places = 0 ) { 4 | let rounded; 5 | 6 | if ( weight ) { 7 | rounded = _.round( weight, places ); 8 | return parseFloat( rounded ).toLocaleString(); 9 | } else { 10 | return 0; 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/assets/stylus/views/recipes.styl: -------------------------------------------------------------------------------- 1 | #recipes { 2 | 3 | h1 { 4 | font-size: 30px; 5 | } 6 | 7 | .marked-display { 8 | p { 9 | line-height: 17px; 10 | margin-bottom: 6px; 11 | 12 | &:last-child { 13 | margin-bottom: 0; 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/assets/stylus/views/status-update.styl: -------------------------------------------------------------------------------- 1 | #status-update { 2 | 3 | .status-update-content { 4 | padding: 10px; 5 | margin-top: 10px; 6 | margin-bottom: 20px; 7 | border: 1px solid #eeeeee; 8 | border-radius: 3px; 9 | } 10 | 11 | .carousel { 12 | margin-bottom: 30px; 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/app/services/validateEmail.js: -------------------------------------------------------------------------------- 1 | import Checkit from 'checkit'; 2 | 3 | export default class { 4 | 5 | constructor( email ) { 6 | this.email = email; 7 | } 8 | 9 | execute() { 10 | let rules = new Checkit( { 11 | email: [ 'email' ] 12 | } ); 13 | 14 | return rules.run( this.email ); 15 | } 16 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/recipe-fatty-acids.styl: -------------------------------------------------------------------------------- 1 | .recipe-fatty-acids 2 | .property-cell 3 | position relative 4 | 5 | .tooltip-question 6 | position absolute 7 | top 0 8 | left 75px 9 | 10 | @media screen and (min-width: 768px) and (max-width: 992px) 11 | left 65px 12 | -------------------------------------------------------------------------------- /src/app/resources/feed.js: -------------------------------------------------------------------------------- 1 | import when from 'when'; 2 | import { get } from 'utils/http'; 3 | 4 | import baseUrl from 'utils/baseUrl'; 5 | 6 | 7 | export function getFeed( options = {} ) { 8 | return when( 9 | get( baseUrl( 'feedables' ), { 10 | params: { 11 | page: options.page 12 | } 13 | } ) 14 | ); 15 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/share-this.styl: -------------------------------------------------------------------------------- 1 | .share-this { 2 | display: table; 3 | 4 | .t-cell { 5 | padding-left: 10px; 6 | vertical-align: top; 7 | 8 | .btn-pinterest { 9 | background-color: #bd081c; 10 | } 11 | 12 | .btn-reddit { 13 | background-color: #ff5700; 14 | } 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/form-save-recipe.styl: -------------------------------------------------------------------------------- 1 | .form-save-recipe { 2 | 3 | .input-description { 4 | padding: 4px; 5 | min-height: 38px; 6 | width: 100%; 7 | } 8 | 9 | .action-buttons { 10 | margin-top: 20px; 11 | } 12 | 13 | .editor { 14 | p { 15 | margin-bottom: 3px; 16 | } 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/app/services/validateComment.js: -------------------------------------------------------------------------------- 1 | import Checkit from 'checkit'; 2 | 3 | export default class { 4 | 5 | constructor( payload ) { 6 | this.payload = payload; 7 | } 8 | 9 | execute() { 10 | let rules = new Checkit( { 11 | comment: [ 'required', 'maxLength:2000' ] 12 | } ); 13 | 14 | return rules.run( this.payload ); 15 | } 16 | } -------------------------------------------------------------------------------- /src/app/resources/imageable.js: -------------------------------------------------------------------------------- 1 | import {resolve} from 'url'; 2 | 3 | import config from 'config'; 4 | 5 | 6 | export function imageableUrl( image ) { 7 | return resolve( config.imageUrl, [ image.path, image.file_name ].join( '/' ) ); 8 | } 9 | 10 | export function imageableThumbUrl( image ) { 11 | return resolve( config.imageUrl, [ image.path, 'thumb-' + image.file_name ].join( '/' ) ); 12 | } -------------------------------------------------------------------------------- /src/app/services/validateRecipeFormFields.js: -------------------------------------------------------------------------------- 1 | import Checkit from 'checkit'; 2 | 3 | export default class { 4 | 5 | constructor( payload ) { 6 | this.payload = payload; 7 | } 8 | 9 | execute() { 10 | let rules = new Checkit( { 11 | name: [ 'required', 'minLength:3', 'maxLength:50' ] 12 | } ); 13 | 14 | return rules.run( this.payload ); 15 | } 16 | } -------------------------------------------------------------------------------- /src/assets/stylus/mixins.styl: -------------------------------------------------------------------------------- 1 | media-common-styles() { 2 | p { 3 | margin-bottom: 5px; 4 | 5 | &:last-child { 6 | margin-bottom: 0; 7 | } 8 | } 9 | 10 | .about { 11 | font-size: 12px; 12 | height: 18px; 13 | } 14 | 15 | span.time, span.link { 16 | margin-left: 5px; 17 | border-bottom: 1px dotted silver; 18 | } 19 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/list-oils-selector.styl: -------------------------------------------------------------------------------- 1 | .list-oils-selector { 2 | .oils-container { 3 | overflow-y: auto; 4 | 5 | td { 6 | &:hover { 7 | cursor: pointer; 8 | background-color: #ebebeb; 9 | } 10 | 11 | &.selected { 12 | background-color: #d5edf2; 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/markdown-editor.styl: -------------------------------------------------------------------------------- 1 | .markdown-editor { 2 | 3 | textarea { 4 | padding: 5px; 5 | line-height: 22px; 6 | 7 | border: 1px solid #e5e5e5; 8 | border-radius: 3px; 9 | } 10 | 11 | .md-header { 12 | padding-bottom: 4px; 13 | } 14 | 15 | .md-preview { 16 | height: 100% !important; 17 | font-size: 16px; 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /deploy/deploy.js: -------------------------------------------------------------------------------- 1 | var utils = require( 'shipit-utils' ); 2 | 3 | 4 | module.exports = function ( gruntOrShipit ) { 5 | require( 'shipit-deploy' )( gruntOrShipit ); 6 | 7 | require( './update' )( gruntOrShipit ); 8 | 9 | utils.registerTask( gruntOrShipit, 'deploy-local', [ 10 | 'deploy:init', 11 | 'deploy:update-local', 12 | 'deploy:publish', 13 | 'deploy:clean' 14 | ] ); 15 | }; -------------------------------------------------------------------------------- /src/app/services/validateProfileForm.js: -------------------------------------------------------------------------------- 1 | import Checkit from 'checkit'; 2 | 3 | export default class { 4 | 5 | constructor( payload ) { 6 | this.payload = payload; 7 | } 8 | 9 | execute() { 10 | let rules = new Checkit( { 11 | name: [ 'required', 'minLength:3', 'maxLength:50' ], 12 | email: [ 'email' ] 13 | } ); 14 | 15 | return rules.run( this.payload ); 16 | } 17 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = false -------------------------------------------------------------------------------- /src/assets/stylus/components/animated-modal.styl: -------------------------------------------------------------------------------- 1 | .animated-modal { 2 | 3 | .close-modal { 4 | cursor: pointer; 5 | text-align: center; 6 | 7 | font-size: 50px; 8 | } 9 | 10 | .animated-modal-content { 11 | padding: 20px; 12 | } 13 | 14 | .fa { 15 | opacity: 0.6; 16 | transition: opacity 0.1s; 17 | 18 | &:hover { 19 | opacity: 1.0; 20 | } 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/feed-item.styl: -------------------------------------------------------------------------------- 1 | .feed-item { 2 | 3 | margin-top: 10px; 4 | margin-bottom: 10px; 5 | 6 | padding: 3px; 7 | border: 1px solid silver + 50%; 8 | border-radius: 3px; 9 | 10 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); 11 | 12 | media-common-styles(); 13 | 14 | .thumbnail { 15 | margin-bottom: 10px; 16 | margin-left: 0 !important; 17 | margin-right: 10px; 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/imageable-edit.styl: -------------------------------------------------------------------------------- 1 | .imageable-edit { 2 | 3 | .thumbnail { 4 | &.deleting { 5 | border-color: red; 6 | } 7 | 8 | img { 9 | margin-top: 8px; 10 | border-radius: 5px; 11 | } 12 | 13 | .caption { 14 | text-align: center; 15 | 16 | p { 17 | margin-bottom: 0; 18 | } 19 | } 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /src/app/services/validatePassword.js: -------------------------------------------------------------------------------- 1 | import Checkit from 'checkit'; 2 | 3 | export default class { 4 | 5 | constructor( password ) { 6 | this.password = password; 7 | } 8 | 9 | execute() { 10 | let rules = new Checkit( { 11 | password: [ 'required', 'alphaDash', 'minLength:6', 'maxLength:20' ] 12 | } ); 13 | 14 | return rules.run( { 15 | password: this.password 16 | } ); 17 | } 18 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/recipe-list-item.styl: -------------------------------------------------------------------------------- 1 | .recipe-list-item { 2 | 3 | padding-bottom: 15px; 4 | 5 | .user-avatar { 6 | float: right; 7 | } 8 | 9 | .recipe-name { 10 | font-size: 20px; 11 | } 12 | 13 | .iodine { 14 | padding-right: 10px; 15 | } 16 | 17 | .properties, .fats, .oils { 18 | 19 | li { 20 | float: left; 21 | padding-right: 10px; 22 | } 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/app/services/validateRequiredField.js: -------------------------------------------------------------------------------- 1 | import Checkit from 'checkit'; 2 | 3 | export default class { 4 | 5 | constructor( fieldName, value ) { 6 | this.fieldName = fieldName; 7 | this.value = value; 8 | } 9 | 10 | execute() { 11 | let rules = new Checkit( { 12 | [this.fieldName]: [ 'required' ] 13 | } ); 14 | 15 | return rules.run( { 16 | [ this.fieldName ]: this.value 17 | } ); 18 | } 19 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/recipe-journal-item.styl: -------------------------------------------------------------------------------- 1 | .recipe-journal-item { 2 | 3 | margin-top: 10px; 4 | margin-bottom: 10px; 5 | 6 | padding: 3px; 7 | border: 1px solid silver + 50%; 8 | border-radius: 3px; 9 | 10 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); 11 | 12 | media-common-styles(); 13 | 14 | .actions { 15 | height: 23px; 16 | } 17 | 18 | &.view { 19 | border: none; 20 | box-shadow: none; 21 | } 22 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/user-avatar.styl: -------------------------------------------------------------------------------- 1 | .user-avatar { 2 | 3 | .avatar-border { 4 | display: inline-block; 5 | 6 | padding: 3px; 7 | 8 | border: 1px solid silver; 9 | border-radius: 3px; 10 | 11 | img { 12 | width: 30px; 13 | height: 30px; 14 | 15 | border-radius: 3px; 16 | } 17 | } 18 | 19 | .fa-user { 20 | padding: 0 5px 0 5px; 21 | font-size: 30px; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/app/views/logout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import authActions from 'actions/auth'; 4 | 5 | export default React.createClass( { 6 | 7 | componentDidMount () { 8 | authActions.logout(); 9 | }, 10 | 11 | render() { 12 | return ( 13 |
14 | 15 |
16 |

Logged Out

17 |
18 | 19 |
20 | ); 21 | } 22 | 23 | } ); -------------------------------------------------------------------------------- /src/app/services/validateLoginFields.js: -------------------------------------------------------------------------------- 1 | import Checkit from 'checkit'; 2 | 3 | export default class { 4 | 5 | constructor( payload ) { 6 | this.payload = payload; 7 | } 8 | 9 | execute() { 10 | let rules = new Checkit( { 11 | username: [ 'required', 'alphaDash', 'minLength:3', 'maxLength:12' ], 12 | password: [ 'required', 'alphaDash', 'minLength:6', 'maxLength:20' ] 13 | } ); 14 | 15 | return rules.run( this.payload ); 16 | } 17 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/navbar-component.styl: -------------------------------------------------------------------------------- 1 | .navbar-component { 2 | 3 | .btn-profile { 4 | padding-top: 6px; 5 | padding-bottom: 6px; 6 | padding-left: 6px; 7 | padding-right: 6px; 8 | 9 | color: white; 10 | background-color: #58A6E6; 11 | } 12 | 13 | .caption { 14 | padding-right: 5px; 15 | 16 | .user-avatar { 17 | height: 25px; 18 | border-radius: 3px; 19 | } 20 | 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/assets/stylus/global.styl: -------------------------------------------------------------------------------- 1 | .strike { 2 | position: relative; 3 | 4 | border-bottom: 1px #ddd solid; 5 | height: 20px; 6 | 7 | margin-bottom: 20px; 8 | 9 | .or { 10 | position: absolute; 11 | left: 48%; 12 | top: 7px; 13 | 14 | padding: 0 3px; 15 | 16 | background-color: #f9f9f9; 17 | } 18 | 19 | } 20 | 21 | .btn-numbered-page, .btn-next-page, .btn-prev-page, .btn-first-page, .btn-last-page { 22 | a { 23 | cursor: pointer; 24 | } 25 | } -------------------------------------------------------------------------------- /src/assets/stylus/views/profile.styl: -------------------------------------------------------------------------------- 1 | #profile { 2 | 3 | .input-description { 4 | width: 100%; 5 | min-height: 40px; 6 | 7 | padding: 5px; 8 | 9 | background-color: transparent; 10 | } 11 | 12 | .username { 13 | padding-bottom: 20px; 14 | 15 | legend { 16 | margin-bottom: 10px; 17 | } 18 | } 19 | 20 | .friend-profile-link { 21 | display: inline-block; 22 | float: left; 23 | margin-right: 5px; 24 | } 25 | 26 | 27 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/button-user-notifications.styl: -------------------------------------------------------------------------------- 1 | .button-user-notifications { 2 | 3 | height: 38px; 4 | margin-right: 5px; 5 | 6 | .badge { 7 | top: -2; 8 | padding: 4px 6px 4px 6px; 9 | margin-left: 3px; 10 | color: #2196f3; 11 | background-color: white; 12 | 13 | font-weight: bold; 14 | } 15 | 16 | .fa { 17 | font-size: 18px; 18 | line-height: 24px; 19 | } 20 | 21 | .fa.active { 22 | color: #ff9800; 23 | 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/app/actions/user.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | import { 4 | getProfile, 5 | getRecipes, 6 | getFriends 7 | } from 'resources/users'; 8 | 9 | let actions = Reflux.createActions( { 10 | getProfile: { asyncResult: true }, 11 | getRecipes: { asyncResult: true }, 12 | getFriends: { asyncResult: true } 13 | } ); 14 | 15 | export default actions; 16 | 17 | actions.getProfile.listenAndPromise( getProfile ); 18 | actions.getRecipes.listenAndPromise( getRecipes ); 19 | actions.getFriends.listenAndPromise( getFriends ); -------------------------------------------------------------------------------- /src/app/components/navLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | import {Link, State} from 'react-router'; 4 | 5 | 6 | export default React.createClass( { 7 | 8 | mixins: [ 9 | State 10 | ], 11 | 12 | activeClassForTo( to ) { 13 | return cx( { active: this.isActive( to ) } ); 14 | }, 15 | 16 | render() { 17 | return ( 18 |
  • {this.props.children}
  • 19 | ); 20 | } 21 | 22 | } ); -------------------------------------------------------------------------------- /src/app/resources/users.js: -------------------------------------------------------------------------------- 1 | import when from 'when'; 2 | import { get } from 'utils/http'; 3 | 4 | import baseUrl from 'utils/baseUrl'; 5 | 6 | 7 | export function getProfile( userId ) { 8 | return when( 9 | get( baseUrl( `users/${userId}` ) ) 10 | ); 11 | } 12 | 13 | export function getRecipes( user ) { 14 | return when( 15 | get( baseUrl( `users/${user.id}/recipes` ) ) 16 | ); 17 | } 18 | 19 | export function getFriends( user ) { 20 | return when( 21 | get( baseUrl( `users/${user.id}/friends` ) ) 22 | ); 23 | } -------------------------------------------------------------------------------- /provision/base.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # install dependencies 4 | sudo apt-get update 5 | sudo apt-get install -y build-essential 6 | 7 | # install node via nvm - https://github.com/creationix/nvm 8 | echo "Installing nvm..." 9 | curl https://raw.githubusercontent.com/creationix/nvm/v0.25.4/install.sh | bash 10 | 11 | echo "source /home/vagrant/.nvm/nvm.sh" >> /home/vagrant/.profile 12 | source /home/vagrant/.profile 13 | 14 | nvm install 0.12.5 15 | nvm alias default 0.12.5 16 | 17 | # install global npm packages 18 | 19 | npm install -g gulp@3.9.0 20 | npm install -g shipit-cli@1.3.0 -------------------------------------------------------------------------------- /src/app/actions/oil.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | import { 4 | getOilById, 5 | 6 | getOilComments, 7 | addCommentToOil 8 | } from 'resources/oils'; 9 | 10 | let actions = Reflux.createActions( { 11 | getOilById: { asyncResult: true }, 12 | 13 | getOilComments: { asyncResult: true }, 14 | addCommentToOil: { asyncResult: true } 15 | } ); 16 | 17 | export default actions; 18 | 19 | actions.getOilById.listenAndPromise( getOilById ); 20 | 21 | actions.getOilComments.listenAndPromise( getOilComments ); 22 | actions.addCommentToOil.listenAndPromise( addCommentToOil ); -------------------------------------------------------------------------------- /src/assets/stylus/components/media-signin-buttons.styl: -------------------------------------------------------------------------------- 1 | .media-signin-buttons { 2 | 3 | .btn-signup { 4 | margin-top: 15px; 5 | } 6 | 7 | .action-button { 8 | margin-top: 15px; 9 | text-align: center; 10 | } 11 | 12 | .link-signup { 13 | padding-top: 10px; 14 | padding-bottom: 10px; 15 | padding-left: 10px; 16 | 17 | width: 250px; 18 | 19 | .action { 20 | font-size: 16px; 21 | } 22 | 23 | .fa { 24 | font-size: 20px; 25 | padding-right: 15px; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/app/stores/myComments.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | import meActions from 'actions/me'; 3 | 4 | export default Reflux.createStore( { 5 | 6 | store: null, 7 | 8 | init() { 9 | this.listenTo( meActions.getComments.completed, gotComments.bind( this ) ); 10 | }, 11 | 12 | getInitialState() { 13 | return this.store; 14 | } 15 | 16 | } ); 17 | 18 | /////////////////// 19 | //// 20 | 21 | function gotComments( comments ) { 22 | this.store = comments; 23 | doTrigger.call( this ); 24 | } 25 | 26 | 27 | function doTrigger() { 28 | this.trigger( this.store ); 29 | } -------------------------------------------------------------------------------- /src/app/stores/userProfile.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | import userActions from 'actions/user'; 3 | 4 | export default Reflux.createStore( { 5 | 6 | store: null, 7 | 8 | init() { 9 | this.listenTo( userActions.getProfile.completed, gotProfile.bind( this ) ); 10 | }, 11 | 12 | getInitialState() { 13 | return this.store; 14 | } 15 | 16 | } ); 17 | 18 | /////////////////// 19 | //// 20 | 21 | function gotProfile( profile ) { 22 | this.store = profile; 23 | doTrigger.call( this ); 24 | } 25 | 26 | 27 | function doTrigger() { 28 | this.trigger( this.store ); 29 | } -------------------------------------------------------------------------------- /src/app/actions/reset.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | import { 4 | requestPasswordReset, 5 | verifyCode, 6 | resetPassword 7 | } from 'resources/resets'; 8 | 9 | let actions = Reflux.createActions( { 10 | //async action 11 | requestPasswordReset: { asyncResult: true }, 12 | verifyCode: { asyncResult: true }, 13 | resetPassword: { asyncResult: true } 14 | } ); 15 | 16 | export default actions; 17 | 18 | actions.requestPasswordReset.listenAndPromise( requestPasswordReset ); 19 | actions.verifyCode.listenAndPromise( verifyCode ); 20 | actions.resetPassword.listenAndPromise( resetPassword ); -------------------------------------------------------------------------------- /src/app/views/account.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {RouteHandler} from 'react-router'; 3 | 4 | import authStore from 'stores/auth'; 5 | 6 | export default React.createClass( { 7 | 8 | statics: { 9 | willTransitionTo( transition ) { 10 | if ( !(authStore.isAuthenticated()) ) { 11 | transition.redirect( 'login', {}, { nextPath: transition.path } ); 12 | } 13 | } 14 | }, 15 | 16 | render() { 17 | return ( 18 |
    19 | 20 |
    21 | ); 22 | } 23 | 24 | } ); -------------------------------------------------------------------------------- /deploy/shipit.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | default: { 3 | workspace: './build', 4 | deployTo: '/var/www/soapee.com/ui/', 5 | rsync: [ '--del' ], 6 | keepReleases: 3, 7 | deleteOnRollback: false 8 | }, 9 | 10 | production: { 11 | servers: [ { 12 | host: '176.58.125.89', 13 | user: 'nazar' 14 | } ], 15 | key: '/home/vagrant/files/deploy/keys/production' 16 | } 17 | }; 18 | 19 | module.exports.config = config; 20 | 21 | module.exports.init = function ( shipit ) { 22 | require( './deploy' )( shipit ); 23 | 24 | shipit.initConfig( config ); 25 | }; -------------------------------------------------------------------------------- /src/app/components/imageableThumbnails.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | 4 | import { imageableThumbUrl } from 'resources/imageable'; 5 | 6 | export default React.createClass( { 7 | 8 | render() { 9 | return ( 10 |
    11 | { _.map( this.props.images, this.renderImage ) } 12 |
    13 | ); 14 | }, 15 | 16 | renderImage( image ) { 17 | return ( 18 | 19 | ); 20 | } 21 | 22 | } ); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | The ReactJS based front end for [Soapee](http://soapee.com), a Soap Lye calculator. 4 | 5 | # Development Environment Provisioning 6 | 7 | Please read the [Provisioning](./doc/provision.md) section for setting up the development environment. 8 | 9 | # Linting 10 | 11 | An [.eslintrc](./eslintrc) is provided for [ESLint](http://eslint.org/) but the eslint packages are not listed in [package.json](./package.json) as 12 | the eslint modules are required for the client machine (and not the development server). 13 | 14 | To install JSX compatible ESLint modules: 15 | 16 | ``` 17 | npm install -g eslint eslint-plugin-react 18 | ``` -------------------------------------------------------------------------------- /src/app/components/markedDisplay.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import marked from 'marked'; 3 | 4 | export default React.createClass( { 5 | 6 | render() { 7 | 8 | return ( 9 |
    14 |
    15 | ); 16 | 17 | }, 18 | 19 | unsafeContent() { 20 | return marked( this.props.content, { 21 | breaks: true, 22 | sanitize: true, 23 | smartypants: true 24 | } ); 25 | } 26 | 27 | } ); -------------------------------------------------------------------------------- /src/app/components/portalSignup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Portal from 'react-portal'; 3 | 4 | import AnimatedModal from 'components/animatedModal'; 5 | 6 | export default React.createClass({ 7 | 8 | render() { 9 | return ( 10 | 16 | 17 | Think of the children 18 | 19 | 20 | ); 21 | } 22 | 23 | }); -------------------------------------------------------------------------------- /src/assets/stylus/components/footer.styl: -------------------------------------------------------------------------------- 1 | .footer { 2 | position: absolute; 3 | bottom: 0; 4 | 5 | padding-top: 5px; 6 | width: 100%; 7 | height: footer-height; 8 | 9 | color: #AAD8FF; 10 | background-color: #2925FF; //##58A6E6 //#4EE9AB //#6966EA //#55ffdc 11 | border-color: transparent; 12 | 13 | box-shadow: 0px -1px 2px rgba(0, 0, 0, 0.3); 14 | 15 | a { 16 | color: #FFC000; 17 | } 18 | 19 | .content { 20 | font-size: 15px; 21 | } 22 | 23 | .share-this { 24 | padding-top: 10px; 25 | 26 | [id^=___plusone] { 27 | width:72px !important; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/app/stores/oil.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | import oilActions from 'actions/oil'; 3 | 4 | import OilModel from 'models/oil'; 5 | 6 | export default Reflux.createStore( { 7 | 8 | store: null, 9 | 10 | init() { 11 | this.listenTo( oilActions.getOilById.completed, gotOil.bind( this ) ); 12 | }, 13 | 14 | getInitialState() { 15 | return this.store; 16 | } 17 | 18 | } ); 19 | 20 | /////////////////// 21 | //// 22 | 23 | function gotOil( oil ) { 24 | this.store = new OilModel( oil ).getExtendedOil(); 25 | 26 | doTrigger.call( this ); 27 | } 28 | 29 | function doTrigger() { 30 | this.trigger( this.store ); 31 | } -------------------------------------------------------------------------------- /src/app/stores/statusUpdate.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | import statusUpdateActions from 'actions/statusUpdate'; 4 | 5 | export default Reflux.createStore( { 6 | 7 | store: null, 8 | 9 | init() { 10 | this.listenTo( statusUpdateActions.getStatusUpdate.completed, gotStatusUpdate.bind( this ) ); 11 | }, 12 | 13 | getInitialState() { 14 | return this.store; 15 | } 16 | 17 | } ); 18 | 19 | ////////////////////////// 20 | //// Private 21 | 22 | function gotStatusUpdate( statusUpdate ) { 23 | this.store = statusUpdate; 24 | doTrigger.call( this ); 25 | } 26 | 27 | function doTrigger() { 28 | this.trigger( this.store ); 29 | } -------------------------------------------------------------------------------- /src/app/components/tooltipQuestion.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tooltip, OverlayTrigger } from 'react-bootstrap'; 3 | 4 | export default React.createClass( { 5 | 6 | getDefaultProps() { 7 | return { 8 | placement: 'left' 9 | }; 10 | }, 11 | 12 | render() { 13 | const tooltip = ( 14 | { this.props.children } 15 | ); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | } ); -------------------------------------------------------------------------------- /src/app/views/application.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Reflux from 'reflux'; 3 | 4 | import {RouteHandler} from 'react-router'; 5 | 6 | import analyticsStore from 'stores/analytics'; 7 | 8 | import NavBar from 'components/navbar'; 9 | import Footer from 'components/footer'; 10 | 11 | export default React.createClass( { 12 | 13 | mixins: [ 14 | Reflux.connect( analyticsStore ) 15 | ], 16 | 17 | render() { 18 | return ( 19 |
    20 | 21 | 22 |
    23 | 24 |
    25 | 26 |
    29 | ); 30 | } 31 | 32 | } ); -------------------------------------------------------------------------------- /src/app/resources/oils.js: -------------------------------------------------------------------------------- 1 | import when from 'when'; 2 | import { get, post } from 'utils/http'; 3 | 4 | import baseUrl from 'utils/baseUrl'; 5 | 6 | 7 | export function getOils() { 8 | return when( 9 | get( baseUrl( 'oils' ) ) 10 | ); 11 | } 12 | 13 | export function getOilComments( oil ) { 14 | return when( 15 | get( baseUrl( `oils/${oil.id}/comments` ) ) 16 | ); 17 | } 18 | 19 | export function addCommentToOil( comment, oil ) { 20 | return when( 21 | post( baseUrl( `oils/${oil.id}/comments` ), { 22 | params: { 23 | comment 24 | } 25 | } ) 26 | ); 27 | } 28 | 29 | 30 | export function getOilById( oilId ) { 31 | return when( 32 | get( baseUrl( `oils/${oilId}` ) ) 33 | ); 34 | } -------------------------------------------------------------------------------- /src/app/actions/auth.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | import { 4 | signupOrLoginThirdParty, 5 | signupLocal, 6 | loginLocal, 7 | logout 8 | } from 'resources/auths'; 9 | 10 | let actions = Reflux.createActions( { 11 | //async action 12 | signupOrLoginThirdParty: { asyncResult: true }, 13 | signupLocal: { asyncResult: true }, 14 | loginLocal: { asyncResult: true }, 15 | logout: { asyncResult: true }, 16 | 17 | //ui actions 18 | SignedUpToLoggedInToSaveRecipe: {} 19 | } ); 20 | 21 | export default actions; 22 | 23 | actions.signupOrLoginThirdParty.listenAndPromise( signupOrLoginThirdParty ); 24 | actions.signupLocal.listenAndPromise( signupLocal ); 25 | actions.loginLocal.listenAndPromise( loginLocal ); 26 | actions.logout.listenAndPromise( logout ); -------------------------------------------------------------------------------- /src/app/stores/myFriendsRecipes.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | import meActions from 'actions/me'; 4 | 5 | export default Reflux.createStore( { 6 | 7 | store: [], 8 | 9 | init() { 10 | this.listenTo( meActions.getMyFriendsRecipes.completed, gotRecipes.bind( this ) ); 11 | this.listenTo( meActions.addFriend.completed, getFriends.bind( this ) ); 12 | }, 13 | 14 | getInitialState() { 15 | return this.store; 16 | } 17 | 18 | } ); 19 | 20 | ////////////////////////// 21 | //// Private 22 | 23 | function getFriends() { 24 | meActions.getMyFriendsRecipes(); 25 | } 26 | 27 | function gotRecipes( recipes ) { 28 | this.store = recipes; 29 | doTrigger.call( this ); 30 | } 31 | 32 | function doTrigger() { 33 | this.trigger( this.store ); 34 | } -------------------------------------------------------------------------------- /src/app/stores/userRecipes.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | import userProfileStore from 'stores/userProfile'; 4 | import userActions from 'actions/user'; 5 | 6 | export default Reflux.createStore( { 7 | 8 | store: [], 9 | 10 | init() { 11 | this.listenTo( userProfileStore, gotUserProfile.bind( this ) ); 12 | this.listenTo( userActions.getRecipes.completed, gotRecipes.bind( this ) ); 13 | }, 14 | 15 | getInitialState() { 16 | return this.store; 17 | } 18 | 19 | } ); 20 | 21 | /////////////////// 22 | //// 23 | 24 | function gotUserProfile( user ) { 25 | userActions.getRecipes( user ); 26 | } 27 | 28 | function gotRecipes( recipes ) { 29 | this.store = recipes; 30 | doTrigger.call( this ); 31 | } 32 | 33 | function doTrigger() { 34 | this.trigger( this.store ); 35 | } -------------------------------------------------------------------------------- /src/app/views/oils.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DocMeta from 'react-doc-meta'; 3 | 4 | import GridOil from 'components/gridOil'; 5 | 6 | export default React.createClass( { 7 | 8 | render() { 9 | document.title = 'Soapee - Oils'; 10 | 11 | return ( 12 |
    13 | 14 | 15 |
    16 | ); 17 | }, 18 | 19 | tags() { 20 | let description = `Soapee Oils Database`; 21 | 22 | return [ 23 | {name: 'description', content: description}, 24 | {name: 'twitter:card', content: description}, 25 | {name: 'twitter:title', content: description}, 26 | {property: 'og:title', content: description} 27 | ]; 28 | } 29 | 30 | 31 | } ); -------------------------------------------------------------------------------- /src/app/stores/calculator.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Reflux from 'reflux'; 3 | 4 | import RecipeModel from 'models/recipe'; 5 | import recipeActions from 'actions/recipe'; 6 | 7 | export default Reflux.createStore( { 8 | 9 | init() { 10 | this.store = new RecipeModel(); 11 | this.store.on( 'calculated', doTrigger.bind( this ) ); 12 | 13 | this.listenTo( recipeActions.resetRecipe, reset.bind( this ) ); 14 | }, 15 | 16 | getInitialState() { 17 | return this.store; 18 | }, 19 | 20 | sapForNaOh( oil ) { //todo - shouldn't really be in here... 21 | return _.round( oil.sap / 1.403, 3 ); 22 | } 23 | 24 | } ); 25 | 26 | ////////////////////// 27 | ///// Private 28 | 29 | function reset() { 30 | this.store.reset(); 31 | } 32 | 33 | function doTrigger() { 34 | this.trigger( this.store ); 35 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/recipe-print-area.styl: -------------------------------------------------------------------------------- 1 | .recipe-print-area { 2 | 3 | position: absolute; 4 | 5 | top: 0; 6 | bottom: 0; 7 | left: 0; 8 | right: 0; 9 | 10 | background-color: white; 11 | color: black !important; 12 | 13 | z-index: 5432; 14 | 15 | font-size: 16px; 16 | 17 | table { 18 | margin-bottom: 0; 19 | 20 | td { 21 | padding: 2px !important; 22 | margin: 0 !important; 23 | } 24 | } 25 | 26 | 27 | .recipe-breakdown, .recipe-totals, .summary { 28 | padding: 5px; 29 | border: 1px solid #e0e0e0; 30 | border-radius: 5px; 31 | } 32 | 33 | .notes { 34 | padding: 10px; 35 | border: 1px solid e0e0e0; 36 | border-radius: 5px; 37 | 38 | p { 39 | margin-bottom: 0.1em; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/assets/stylus/views/my-status-updates.styl: -------------------------------------------------------------------------------- 1 | #my-status-updates { 2 | 3 | .markdown-editor { 4 | textarea { 5 | width: 50%; 6 | } 7 | } 8 | 9 | .add-status-update { 10 | padding-bottom: 40px; 11 | } 12 | 13 | .status-update { 14 | margin-top: 10px; 15 | margin-bottom: 10px; 16 | 17 | padding: 3px; 18 | border: 1px solid silver + 50%; 19 | border-radius: 3px; 20 | 21 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); 22 | 23 | p { 24 | margin-bottom: 5px; 25 | 26 | &:last-child { 27 | margin-bottom: 0; 28 | } 29 | } 30 | } 31 | 32 | .actions { 33 | span { 34 | margin-left: 10px; 35 | } 36 | } 37 | 38 | .dropzone { 39 | margin-bottom: 20px; 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/user-notifications.styl: -------------------------------------------------------------------------------- 1 | .user-notifications { 2 | 3 | width: 260px; 4 | 5 | .user-notification { 6 | padding: 6px; 7 | 8 | padding-top: 10px; 9 | padding-bottom: 10px; 10 | 11 | border-bottom: 1px solid #f1f1f1; 12 | 13 | &:last-child { 14 | border-bottom: none; 15 | } 16 | 17 | &.unread { 18 | background-color: #58A6E6 + 95%; 19 | } 20 | 21 | .actions { 22 | padding-top: 5px; 23 | 24 | .btn { 25 | padding-top: 0; 26 | padding-bottom: 0; 27 | 28 | .fa { 29 | margin-right: 10px; 30 | } 31 | } 32 | } 33 | 34 | .user-avatar { 35 | float: left; 36 | padding-right: 5px; 37 | } 38 | 39 | } 40 | 41 | 42 | } -------------------------------------------------------------------------------- /src/app/views/printCalculation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Reflux from 'reflux'; 3 | import { Navigation, State } from 'react-router'; 4 | 5 | import calculatorStore from 'stores/calculator'; 6 | 7 | import RecipePrintArea from 'components/recipePrintArea'; 8 | 9 | export default React.createClass( { 10 | 11 | mixins: [ 12 | Navigation, 13 | State, 14 | Reflux.connect( calculatorStore, 'recipe' ) 15 | ], 16 | 17 | componentDidMount() { 18 | if ( this.getQuery().preview !== 'true' ) { 19 | window.print(); 20 | this.replaceWith( 'calculator' ); 21 | } 22 | }, 23 | 24 | render() { 25 | return ( 26 | 31 | ); 32 | } 33 | 34 | } ); -------------------------------------------------------------------------------- /src/app/resources/resets.js: -------------------------------------------------------------------------------- 1 | import when from 'when'; 2 | 3 | import { post } from 'utils/http'; 4 | import baseUrl from 'utils/baseUrl'; 5 | 6 | export function requestPasswordReset( email ) { 7 | return when( 8 | post( baseUrl( 'resets/request-reset' ), { 9 | params: { 10 | email 11 | } 12 | } ) 13 | ); 14 | } 15 | 16 | export function verifyCode( token, code ) { 17 | return when( 18 | post( baseUrl( 'resets/verify' ), { 19 | params: { 20 | token, 21 | code 22 | } 23 | } ) 24 | ); 25 | } 26 | 27 | export function resetPassword( token, code, password ) { 28 | return when( 29 | post( baseUrl( 'resets/reset-password' ), { 30 | params: { 31 | token, 32 | code, 33 | password 34 | } 35 | } ) 36 | ); 37 | } -------------------------------------------------------------------------------- /src/assets/stylus/components/sap-calculator.styl: -------------------------------------------------------------------------------- 1 | .sap-calculator { 2 | 3 | .list-oils-selector { 4 | .oils-container { 5 | height: 250px; 6 | max-height: 250px; 7 | } 8 | } 9 | 10 | .properties-oil, .recipe-oils-container { 11 | max-height: 285px; 12 | overflow-y: auto; 13 | } 14 | 15 | .recipe-oils { 16 | input { 17 | height: 25px; 18 | } 19 | } 20 | 21 | .panel-heading { 22 | .openByClickOn { 23 | float: right; 24 | } 25 | } 26 | 27 | #mixed-fields { 28 | .left-spacer { 29 | padding-left: 20px; 30 | } 31 | 32 | .alert { 33 | margin-top: 20px; 34 | padding: 4px 5px; 35 | } 36 | } 37 | 38 | .weights-water { 39 | padding-top: 5px; 40 | } 41 | 42 | .superfat-after { 43 | padding-top: 5px; 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/app/components/facebookComments.js: -------------------------------------------------------------------------------- 1 | /*global FB*/ 2 | 3 | import React from 'react/addons'; 4 | 5 | export default React.createClass( { 6 | 7 | mixins: [ 8 | React.addons.PureRenderMixin 9 | ], 10 | 11 | componentDidMount() { 12 | this.initialiseCommentsWidget(); 13 | }, 14 | 15 | componentDidUpdate() { 16 | this.initialiseCommentsWidget(); 17 | }, 18 | 19 | render() { 20 | return ( 21 |
    22 |
    26 |
    27 |
    28 | ); 29 | }, 30 | 31 | initialiseCommentsWidget() { 32 | $( this.getDOMNode() ).find( '.fb-comments' ).empty(); 33 | FB.XFBML.parse( $( this.getDOMNode() ).get( 0 ) ); 34 | } 35 | 36 | 37 | } ); -------------------------------------------------------------------------------- /src/app/stores/myStatusUpdates.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | import meActions from 'actions/me'; 4 | import statusUpdateActions from 'actions/statusUpdate'; 5 | 6 | export default Reflux.createStore( { 7 | 8 | store: [], 9 | 10 | init() { 11 | this.listenTo( meActions.getMyStatusUpdates.completed, gotStatusUpdates.bind( this ) ); 12 | this.listenTo( statusUpdateActions.addStatusUpdate.completed, gotNewStatusUpdate.bind( this ) ); 13 | }, 14 | 15 | getInitialState() { 16 | return this.store; 17 | } 18 | 19 | } ); 20 | 21 | ////////////////////////// 22 | //// Private 23 | 24 | function gotStatusUpdates( statusUpdates ) { 25 | this.store = statusUpdates; 26 | doTrigger.call( this ); 27 | } 28 | 29 | function gotNewStatusUpdate( statusUpdate ) { 30 | this.store.unshift( statusUpdate ); 31 | doTrigger.call( this ); 32 | } 33 | 34 | function doTrigger() { 35 | this.trigger( this.store ); 36 | } -------------------------------------------------------------------------------- /src/app/app.js: -------------------------------------------------------------------------------- 1 | /*global FB*/ 2 | 3 | //load CSS assets first 4 | require( '../assets/main.css' ); 5 | 6 | import _ from 'lodash'; 7 | import React from 'react'; 8 | import Reflux from 'reflux'; 9 | import Router from 'react-router'; 10 | import ga from 'react-ga'; 11 | 12 | import config from 'config'; 13 | import routes from './routes'; 14 | 15 | /////////////////// 16 | /// INITIALISE 17 | 18 | let analytics = _.get( config, 'analytics.google' ); 19 | 20 | if ( analytics ) { 21 | ga.initialize( analytics ); 22 | } 23 | 24 | FB.init( { 25 | appId: config.auth.facebookClientId, 26 | cookie: true, 27 | version: 'v2.10', 28 | xfbml: true 29 | } ); 30 | 31 | Reflux.setPromiseFactory( require( 'when' ).promise ); 32 | 33 | Router.run( routes, Router.HistoryLocation, function ( Handler, state ) { 34 | if ( analytics ) { 35 | ga.pageview( state.pathname ); 36 | } 37 | 38 | React.render( , document.getElementById( 'application' ) ); 39 | } ); -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | var path = require( 'path' ); 2 | var webpack = require( 'webpack' ); 3 | 4 | var webpackDevConfig = { 5 | overrides: { 6 | devtool: 'eval', 7 | debug: true, 8 | entry: { 9 | app: [ 10 | 'webpack-dev-server/client?http://192.168.30.20:8080', 11 | 'webpack/hot/only-dev-server', 12 | './src/app/app.js' 13 | ] 14 | } 15 | }, 16 | 17 | plugins: [ 18 | new webpack.DefinePlugin( { 19 | 'process.env': { 20 | NODE_ENV: JSON.stringify( 'development' ) 21 | } 22 | } ) 23 | ], 24 | 25 | loaders: [ 26 | { 27 | test: /\.jsx?$/, 28 | loaders: [ 'react-hot', 'babel' ], 29 | include: path.join( __dirname, 'src', 'app' ), 30 | exclude: path.join( __dirname, 'node_modules' ) 31 | } 32 | ] 33 | }; 34 | 35 | module.exports = require( './webpack.config' )( webpackDevConfig ); -------------------------------------------------------------------------------- /src/app/stores/myFriends.js: -------------------------------------------------------------------------------- 1 | import Reflux from 'reflux'; 2 | 3 | import authStore from 'stores/auth'; 4 | import meActions from 'actions/me'; 5 | 6 | export default Reflux.createStore( { 7 | 8 | store: [], 9 | 10 | init() { 11 | this.listenTo( authStore, authenticated.bind( this ) ); 12 | this.listenTo( meActions.getMyFriends.completed, gotMyFriends.bind( this ) ); 13 | this.listenTo( meActions.addFriend.completed, getFriends.bind( this ) ); 14 | }, 15 | 16 | getInitialState() { 17 | return this.store; 18 | } 19 | 20 | } ); 21 | 22 | ////////////////////////// 23 | //// Private 24 | 25 | function authenticated( user ) { 26 | if ( user && user.id ) { 27 | getFriends(); 28 | } 29 | } 30 | 31 | function getFriends() { 32 | meActions.getMyFriends(); 33 | } 34 | 35 | function gotMyFriends( friends ) { 36 | this.store = friends; 37 | doTrigger.call( this ); 38 | } 39 | 40 | function doTrigger() { 41 | this.trigger( this.store ); 42 | } -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure(2) do |config| 5 | config.vm.box = "bento/debian-7.8-i386" 6 | config.vm.box_version = "2.2.0" 7 | config.vm.network :forwarded_port, guest: 22, host: 3020, id: 'ssh' 8 | 9 | # 192.168.30.20 - UI server 10 | # 192.168.30.21 - api server 11 | config.vm.network "private_network", ip: "192.168.30.20" 12 | 13 | #RSYNC based one-way file sharing 14 | config.vm.synced_folder ".", "/home/vagrant/files", 15 | type: "rsync", 16 | rsync__exclude: [".git/", "node_modules", ".idea"], 17 | rsync__auto: false 18 | 19 | config.vm.provider "virtualbox" do |vb| 20 | # Customize the amount of memory on the VM: 21 | vb.memory = "384" 22 | vb.cpus = 1 23 | end 24 | 25 | # provision VM 26 | config.vm.provision "shell", path: "provision/base.sh", privileged: false 27 | 28 | if RUBY_PLATFORM =~ /mingw/ 29 | config.vm.provision "shell", path: "provision/client.sh", privileged: false 30 | end 31 | 32 | 33 | end 34 | -------------------------------------------------------------------------------- /src/app/stores/recipes.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Reflux from 'reflux'; 3 | 4 | import recipeActions from 'actions/recipe'; 5 | 6 | export default Reflux.createStore( { 7 | 8 | store: [], 9 | count: 0, 10 | 11 | init() { 12 | this.listenTo( recipeActions.getRecipes, reset.bind( this ) ); 13 | this.listenTo( recipeActions.getRecipes.completed, gotRecipes.bind( this ) ); 14 | }, 15 | 16 | getInitialState() { 17 | return this.store; 18 | }, 19 | 20 | ///public methods 21 | 22 | totalPages() { 23 | return _.ceil( this.count / 10 ); 24 | } 25 | 26 | } ); 27 | 28 | ////////////////////////// 29 | //// Private 30 | 31 | function reset() { 32 | this.store = []; 33 | 34 | doTrigger.call( this ); 35 | } 36 | 37 | function gotRecipes( data ) { 38 | this.store = data.recipes; 39 | this.count = Number( data.count ); 40 | 41 | doTrigger.call( this ); 42 | } 43 | 44 | function doTrigger() { 45 | this.trigger( this.store ); 46 | } 47 | -------------------------------------------------------------------------------- /src/assets/stylus/views/user-profile.styl: -------------------------------------------------------------------------------- 1 | h1-font-size = 30px; 2 | 3 | #user-profile { 4 | 5 | .profile-avatar { 6 | .user-avatar { 7 | float: right; 8 | padding-right: 20px; 9 | padding-top: 20px; 10 | } 11 | 12 | } 13 | 14 | .jumbotron.compact { 15 | padding-top: 10px; 16 | padding-left: 20px; 17 | 18 | h1 { 19 | font-size: h1-font-size; 20 | } 21 | 22 | h2 { 23 | font-size: h1-font-size - 6px; 24 | } 25 | 26 | h3, h4, h5, h6 { 27 | font-size: h1-font-size - 10px; 28 | } 29 | } 30 | 31 | .friends { 32 | .button-add-friend { 33 | float: left; 34 | margin-right: 5px; 35 | } 36 | .friend-profile-link { 37 | display: inline-block; 38 | float: left; 39 | margin-right: 5px; 40 | } 41 | } 42 | 43 | .user-recipes { 44 | padding-top: 20px; 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/app/components/recipesLinkTable.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | import { Link } from 'react-router'; 4 | 5 | export default React.createClass( { 6 | 7 | render() { 8 | return ( 9 |
    10 | 11 | 12 | 15 | 16 | 17 | { _.map( this.props.recipes, this.renderRow ) } 18 | 19 |
    13 | Recipe Name 14 |
    20 |
    21 | ); 22 | }, 23 | 24 | renderRow( recipe ) { 25 | return ( 26 | 27 | { recipe.name } 28 | 29 | ); 30 | } 31 | 32 | } ); -------------------------------------------------------------------------------- /src/app/stores/feed.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Reflux from 'reflux'; 3 | 4 | import feedActions from 'actions/feed'; 5 | 6 | export default Reflux.createStore( { 7 | 8 | store: [], 9 | count: 0, 10 | 11 | init() { 12 | this.listenTo( feedActions.getFeed, reset.bind( this ) ); 13 | this.listenTo( feedActions.getFeed.completed, gotFeed.bind( this ) ); 14 | }, 15 | 16 | getInitialState() { 17 | return this.store; 18 | }, 19 | 20 | totalPages() { 21 | return _.ceil( this.count / 15 ); 22 | }, 23 | 24 | pagerVisible() { 25 | return this.count > 15; 26 | } 27 | 28 | } ); 29 | 30 | ////////////////////////// 31 | //// Private 32 | 33 | function reset() { 34 | this.store = []; 35 | doTrigger.call( this ); 36 | } 37 | 38 | function gotFeed( feed ) { 39 | this.store = feed.feed; 40 | this.count = Number( feed.count ); 41 | 42 | doTrigger.call( this ); 43 | } 44 | 45 | function doTrigger() { 46 | this.trigger( this.store ); 47 | } -------------------------------------------------------------------------------- /src/app/stores/userFriends.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import Reflux from 'reflux'; 3 | 4 | import userProfileStore from 'stores/userProfile'; 5 | import authStore from 'stores/auth'; 6 | 7 | import userActions from 'actions/user'; 8 | 9 | export default Reflux.createStore( { 10 | 11 | store: [], 12 | 13 | init() { 14 | this.listenTo( userProfileStore, gotUserProfile.bind( this ) ); 15 | this.listenTo( userActions.getFriends.completed, gotFriends.bind( this ) ); 16 | }, 17 | 18 | getInitialState() { 19 | return this.store; 20 | }, 21 | 22 | iAmNotFriendOfUser() { 23 | return _.filter( this.store, { id: authStore.userId() } ).length === 0; 24 | } 25 | 26 | } ); 27 | 28 | /////////////////// 29 | //// 30 | 31 | function gotUserProfile( user ) { 32 | userActions.getFriends( user ); 33 | } 34 | 35 | function gotFriends( users ) { 36 | this.store = users; 37 | doTrigger.call( this ); 38 | } 39 | 40 | function doTrigger() { 41 | this.trigger( this.store ); 42 | } -------------------------------------------------------------------------------- /src/app/components/markdownEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import marked from 'marked'; 3 | 4 | import TextArea from 'react-textarea-autosize'; 5 | 6 | export default React.createClass( { 7 | 8 | componentDidMount() { 9 | $( this.getDOMNode() ).find( 'textarea' ).markdown( { 10 | iconlibrary: 'fa', 11 | fullscreen: { 12 | enable: false 13 | }, 14 | resize: 'both', 15 | hiddenButtons: [ 'cmdCode' ], 16 | 17 | onPreview: this.preview 18 | } ); 19 | }, 20 | 21 | render() { 22 | return ( 23 |
    24 |