text:
import _ from 'lodash';
import React from 'react';
import Reflux from 'reflux';
import { Navigation } from 'react-router';

import recipeActions from 'actions/recipe';
import recipeStore from 'stores/recipe';

import SapCalculator from 'components/sapCalculator';
import FormSaveRecipe from 'components/formSaveRecipe';
import Imageable from 'components/imageable';
import ImageableEdit from 'components/imageableEdit';

export default React.createClass( {

    statics: {
        willTransitionTo: function ( transition, params ) {
            recipeActions.getRecipeById( params.id );
        }
    },

    mixins: [
        Navigation,
        Reflux.connect( recipeStore, 'recipe' )
    ],

    render() {
        document.title = 'Soapee - Edit';

        return (

                <SapCalculator
                    recipe={ this.state.recipe }
                    />

                { _.get(this.state, 'recipe.recipe.images.length' ) > 0  &&

                            <legend>Delete Photos?</legend>
                            <ImageableEdit
                                images={ this.state.recipe.recipe.images }
                                />

                }

                { this.state.recipe.countWeights() > 0 &&

                        <FormSaveRecipe
                            recipe={ this.state.recipe }
                            buttonCancel={ true }
                            buttonCaptionSave={ this.saveCaption() }
                            buttonDisabledSave={ this.state.saving }
                            onSave={ this.saveRecipe }
                            onSaveAs={ this.saveAsRecipe }
                            onCancel={ this.goBackToView }
                            />

                }

        );
    },

    startImageUploadHookFn( fnToStartUploads ) {
        this.startUploads = fnToStartUploads;
    },

    saveCaption() {
        return this.state.saving ? 'Saving Recipe' : 'Save Recipe';
    },

    saveRecipe() {
        return this.doSaveAction( recipeActions.updateRecipe );
    },

    saveAsRecipe() {
        return this.doSaveAction( recipeActions.createRecipe );
    },

    doSaveAction( action ) {
        this.setState( {
            saving: true
        } );

        recipeStore.calculate();

        return action( this.state.recipe )
            .then( this.toRecipeView.bind( this ) )
            .finally(() => this.setState({
                saving: false
            }));

        function uploadImages() {
            this.startUploads( this.state.recipe.getModelValue( 'id' ) );
        }
    },

    printRecipe() {
        this.replaceWith( 'printRecipe', { id: this.state.recipe.getModelValue( 'id' ) } );
    },

    goBackToView() {
        this.toRecipeView( this.state.recipe.recipe );
    },

    toRecipeView(recipe) {
        this.transitionTo( 'recipe', { id: recipe.id} );
    }

} );


text:
import _ from 'lodash';
import React from 'react/addons';
import cx from 'classnames';
import { Button } from 'react-bootstrap';
import { Navigation } from 'react-router';

import resetActions from 'actions/reset';

import authStore from 'stores/auth';

import ValidateEmail from 'services/validateEmail';
import ValidateRequiredField from 'services/validateRequiredField';
import ValidatePassword from 'services/validatePassword';

export default React.createClass( {

    mixins: [
        React.addons.LinkedStateMixin,
        Navigation
    ],

    getInitialState() {
        return {
            step: 0,
            errors: {}
        };
    },

    render() {
        return (

                <legend>Password Reset</legend>

Step 1 - Send Reset Email

Enter the email address used when you registered with username and password.

You'll be sent a reset code to change your password.

                                Email Address
                                <input className="form-control"
                                       type="text"
                                       valueLink={ this.linkState( 'email' ) }
                                    />

                                { this.renderErrorsFor( 'email' ) }

                            <Button bsStyle="primary" onClick={ this.requestReset }>Send Reset Email</Button>

                { this.renderResetStep() }
                { this.renderPasswordReset() }

        );
    },

    renderResetStep() {
        if ( this.state.step > 0 ) {
            return (

Step 2 - Reset Code

An email has been sent containing your reset code. Enter the reset code to proceed.

Please also check your spam folder just in case the reset email ended up there.

                                Reset Code
                                <input className="form-control"
                                       type="text"
                                       valueLink={ this.linkState( 'code' ) }
                                    />

                                { this.renderErrorsFor( 'code' ) }

                            <Button bsStyle="primary" onClick={ this.verifyCode }>Verify Reset Code</Button>

            );
        }
    },

    renderPasswordReset() {
        if ( this.state.step > 1 ) {
            return (

Step 3 - Password Reset

Enter your new password.

                                Password
                                <input className="form-control"
                                       type="text"
                                       valueLink={ this.linkState( 'password' ) }
                                    />

                            { this.renderErrorsFor( 'password' ) }

                            <Button bsStyle="primary" onClick={ this.resetPassword }>Reset Password</Button>

            );
        }
    },

    renderErrorsFor( field ) {
        if ( this.state.errors[ field ] ) {
            return (
                { this.state.errors[ field ]}
            );
        }
    },

    requestReset() {
        this.resetErrors();

        validateEmail.call( this )
            .with( this )
            .then( requestReset )
            .then( setTokenAndStep1 )
            .catch( errors );

        function validateEmail() {

            return validateExists.call( this )
                .then( validateFormat.bind( this ) );

            function validateFormat() {
                return new ValidateEmail( this.email() )
                    .execute();
            }

            function validateExists() {
                return new ValidateRequiredField( 'email', this.email() )
                    .execute();

            }
        }

        function requestReset() {
            return resetActions.requestPasswordReset( this.email() );
        }

        function setTokenAndStep1( data ) {
            this.setState( {
                token: data.token,
                step: 1
            } );
        }

        function errors( e ) {
            if ( e.name === 'CheckitError' ) {
                this.setState( {
                    errors: e.toJSON()
                } );
            } else {
                let error = e.responseJSON;
                let message;

                if ( error.message === 'Record not found' ) {
                    message = 'Could not find an account with this email';
                } else {
                    message = error.message;
                }

                this.setState( {
                    errors: {
                        email: message
                    }
                } );
            }
        }

    },

    verifyCode() {
        this.resetErrors();

        validateCodeField.call( this )
            .with( this )
            .then( validateCode )
            .then( toStep2 )
            .catch( errors );

        function validateCodeField() {
            return new ValidateRequiredField( 'code', this.code() )
                .execute();
        }

        function validateCode() {
            return resetActions.verifyCode( this.state.token, this.code() );
        }

        function toStep2() {
            this.setState( {
                step: 2
            } );
        }

        function errors( e ) {
            if ( e.name === 'CheckitError' ) {
                this.setState( {
                    errors: e.toJSON()
                } );
            } else {
                let error = e.responseJSON;

                if ( error.errorType === 'RecordNotFound' ) {
                    this.setState( {
                        errors: {
                            email: 'Invalid Reset Code. Please check your Reset Code and try again.'
                        }
                    } );
                }
            }
        }
    },

    resetPassword() {
        this.resetErrors();

        validatePassword.call( this )
            .with( this )
            .then( requestPasswordChange )
            .then( toProfile )
            .catch( errors );

        function validatePassword() {
            return new ValidatePassword( this.password() )
                .execute();
        }

        function requestPasswordChange() {
            return resetActions.resetPassword( this.state.token, this.code(), this.password() );
        }

        function toProfile() {
            setTimeout( () => {
                authStore.authenticate()
                    .then( () => this.transitionTo( 'profile' ) );
            } );
        }

        function errors( e ) {
            if ( e.name === 'CheckitError' ) {
                this.setState( {
                    errors: e.toJSON()
                } );
            } else {
                let error = e.responseJSON;

                this.setState( {
                    errors: {
                        password: error.message
                    }
                } );
            }
        }

    },

    email() {
        return _.trim( this.state.email );
    },

    code() {
        return _.trim( this.state.code );
    },

    password() {
        return _.trim( this.state.password );
    },

    resetErrors() {
        this.setState( {
            errors: {}
        } );
    },

    formClassNames( field ) {
        return cx( 'form-group', {
            'has-error': this.state.errors[ field ]
        } );
    }

} );

repo_name: nazar/soapee-ui

repo_language: JavaScript

mime_type: text/plain


text:
import React from 'react';
import {RouteHandler} from 'react-router';

import authStore from 'stores/auth';

export default React.createClass( {

    statics: {
        willTransitionTo( transition ) {
            if ( !(authStore.isAuthenticated()) ) {
                transition.redirect( 'login', {}, { nextPath: transition.path } );
            }
        }
    },

    render() {
        return (
            <div id="account">
                <RouteHandler/>
            </div>
        );
    }

} );


text:
import _ from 'lodash';
import React from 'react/addons';
import Reflux from 'reflux';
import cx from 'classnames';

import formLinkHandlers from 'mixins/formLinkHandlers';
import ValidateProfileForm from 'services/validateProfileForm';

import meStore from 'stores/me';
import authStore from 'stores/auth';
import myFriendsStore from 'stores/myFriends';

import meActions from 'actions/me';

import UserAvatar from 'components/userAvatar';
import MarkdownEditor from 'components/markdownEditor';

export default React.createClass( {

    mixins: [
        Reflux.connect( meStore, 'profile' ),
        Reflux.connect( myFriendsStore, 'friends' ),
        React.addons.LinkedStateMixin,
        formLinkHandlers
    ],

    getInitialState() {
        return {
            errors: {}
        };
    },

    componentDidMount() {
        meActions.getMyProfile();
    },

    render() {
        document.title = 'Soapee - My Profile';

        return (

                <legend>My Profile</legend>

                    <form className="form-horizontal" onSubmit={ (e) => e.preventDefault() }>

                                { this.renderUsername() }

                                <legend>My Name</legend>
                                <input type="text"
                                       className="form-control"
                                       valueLink={ this.linkStore( meStore, 'name' ) }
                                    />

                                { this.state.errors.name &&
                                    { this.state.errors.name[ 0 ]}
                                }

                                <legend>My Email</legend>
                                <input type="text"
                                       className="form-control"
                                       valueLink={ this.linkStore( meStore, 'email' ) }
                                    />

                                { this.state.errors.email &&
                                    { this.state.errors.email[ 0 ]}
                                }

                            <UserAvatar
                                user={ this.state.profile }
                                />

                                <legend>About Me</legend>
                                <MarkdownEditor
                                    className="input-description"
                                    useCacheForDOMMeasurements
                                    valueLink={ this.linkStore( meStore, 'about' ) }
                                    rows={ 2 }
                                    />

                                    <button className="btn btn-primary" onClick={ this.updateProfile }>Update Profile</button>
                                    Invite a friend to Soapee

                    </form>

                { this.renderMyFriends() }

        );
    },

    renderMyFriends() {
        function renderFriend( user ) {
            return (
                <a href={ `/users/${user.id}` } className="friend-profile-link">
                    <UserAvatar
                        user={ user }
                        showTooltip={ true }
                        />

            );
        }

        if ( this.state.friends.length ) {
            return (

                    <legend>My Friends</legend>
                    { _.map( this.state.friends, renderFriend, this ) }

            );
        }
    },

    renderUsername() {
        let profile = this.state.profile;

        if ( profile.verifications && profile.verifications.length === 1 ) {
            return (

                    <legend>My Username</legend>
                    { profile.verifications[0].provider_id }

            );
        }
    },

    formClassNames( field ) {
        return cx( 'form-group', {
            'has-error': this.state.errors[ field ]
        } );
    },

    updateProfile() {
        this.setState( {
            errors: {}
        } );

        validateForm.call( this )
            .with( this )
            .then( save )
            .then( successNotification )
            .catch( setErrors );

        function validateForm() {
            return new ValidateProfileForm( {
                name: this.state.profile.name,
                email: this.state.profile.email
            } )
                .execute();
        }

        function save() {
            return meActions.updateMyProfile( {
                name: this.state.profile.name,
                email: this.state.profile.email,
                about: this.state.profile.about
            } );
        }

        function successNotification() {
            $.bootstrapGrowl( 'Profile Updated', { type: 'warning', delay: 5000 } );
        }

        function setErrors( e ) {
            if ( e.name === 'CheckitError' ) {
                this.setState( {
                    errors: e.toJSON()
                } );
            }
        }

    },

    mailToLink() {
        let subject;
        let body;

        subject = `Check out the Soapee calculator and soap recipes`;
        body = `You can register on Soapee here: http://soapee.com/signup

        Once you've registered, check out my Soapee profile here: http://soapee.com/users/${authStore.userId()} and press the "Add Friend" button to connect with me.

        You'll be able to see my soap recipes once you've connected.

        ${authStore.userName()}
        `;

        body = encodeURIComponent(body);

        return `mailto:?subject=${subject}&body=${body}`;
    }

} );


text:
import React from 'react';
import Reflux from 'reflux';
import { Navigation, State } from 'react-router';

import calculatorStore from 'stores/calculator';

import RecipePrintArea from 'components/recipePrintArea';

export default React.createClass( {

    mixins: [
        Navigation,
        State,
        Reflux.connect( calculatorStore, 'recipe' )
    ],

    componentDidMount() {
        if ( this.getQuery().preview !== 'true' ) {
            window.print();
            this.replaceWith( 'calculator' );
        }
    },

    render() {
        return (
            <div id="print-calculation">
                <RecipePrintArea
                    recipe={ this.state.recipe }
                    />
            </div>
        );
    }

} );


text:
import React from 'react';
import Reflux from 'reflux';
import { Navigation, State } from 'react-router';

import MediaSigninButtons from 'components/mediaSigninButtons';
import LocalSignupForm from 'components/localSignupForm';

import authStore from 'stores/auth';

export default React.createClass( {

    mixins: [
        Navigation,
        State,
        Reflux.connect( authStore, 'auth' )
    ],

    getInitialState() {
        return {
            errors: {}
        };
    },

    componentWillUpdate() {
        let path = this.getQuery().nextPath || 'profile';

        if ( authStore.isAuthenticated() ) {
            this.replaceWith( path );
        }
    },

    render() {
        document.title = 'Soapee - Signup';

        return (

Start Using Soapee

Create a free account to save and share all your recipes

                            <LocalSignupForm />

OR

                            <MediaSigninButtons />

        );
    }

} );


text:
import React from 'react';

import authActions from 'actions/auth';

export default React.createClass( {

    componentDidMount () {
        authActions.logout();
    },

    render() {
        return (

Logged Out

        );
    }

} );


text:
import _ from 'lodash';
import React from 'react/addons';
import Reflux from 'reflux';
import { State, Navigation } from 'react-router';

import statusUpdateActions from 'actions/statusUpdate';

import statusUpdateStore from 'stores/statusUpdate';
import authStore from 'stores/auth';

import formLinkHandlers from 'mixins/formLinkHandlers';

import MarkdownEditor from 'components/markdownEditor';
import Imageable from 'components/imageable';
import ImageableEdit from 'components/imageableEdit';

export default React.createClass( {

    mixins: [
        Navigation,
        State,
        React.addons.LinkedStateMixin,
        formLinkHandlers,
        Reflux.connect( statusUpdateStore, 'statusUpdate' )
    ],

    componentDidMount() {
        statusUpdateActions.getStatusUpdate( this.getParams().id )
            .then( this.checkIfThisIsMyStatusUpdate );
    },

    render() {
        if ( this.state.error ) {
            return (

                    { this.renderError() }

            );
        } else {
            return (

                    { this.renderStatusUpdate() }

            );
        }
    },

    renderStatusUpdate() {
        return (

                <legend>Edit Status Update</legend>
                <MarkdownEditor
                    placeholder="Add your status update...."
                    valueLink={ this.linkComponentState( this, 'statusUpdate.update' ) }
                    rows={ 4 }
                    />

                { _.get( this.state, 'statusUpdate.images.length' ) > 0 &&

                            <legend>Delete Photos?</legend>
                            <ImageableEdit
                                images={ this.state.statusUpdate.images }
                                />

                }

                    <button className="btn btn-primary"
                            onClick={ this.updateStatusUpdate }
                            disabled={ this.state.saving }
                        >
                         Save
                    </button>
                    <button className="btn btn-primary"
                            onClick={ this.clearAndRedirect }
                        >
                         Cancel
                    </button>
                    <button className="btn btn-warning"
                            onClick={ this.deleteStatusUpdate }
                        >
                         Delete
                    </button>

                { this.state.errors &&

                    { this.state.errors }

                }

        );
    },

    renderError() {
        return (

{ this.state.error }

        );
    },

    startImageUploadHookFn( startImageUploadFn ) {
        this.startImageUpload = startImageUploadFn;
    },

    updateStatusUpdate() {
        this.setState( {
            saving: true
        } );

        statusUpdateActions.updateStatusUpdate( this.state.statusUpdate )
            .then( () => this.startImageUpload( this.state.statusUpdate.id ) );
    },

    deleteStatusUpdate() {
        if ( confirm( 'Delete this status update?' ) ) {
            statusUpdateActions.deleteStatusUpdate( this.state.statusUpdate )
                .then( () => this.transitionTo( 'my-status-updates' ) );
        }
    },

    clearAndRedirect() {
        this.setState( {
            saving: false
        } );

        this.transitionTo( 'status-update', { id: this.state.statusUpdate.id } );
    },

    checkIfThisIsMyStatusUpdate( statusUpdate ) {
        if ( !(authStore.isMyId( statusUpdate.user_id )) ) {
            this.setState( {
                error: 'Sorry, cannot edit another user\'s status update.'
            } );
        }
    }

} );


text:
import _ from 'lodash';
import React from 'react';
import Reflux from 'reflux';
import Pager from 'react-pager';

import feedActions from 'actions/feed';
import feedStore from 'stores/feed';

import FeedItem from 'components/feedItem';
import Spinner from 'components/spinner';

export default React.createClass( {

    statics: {
        willTransitionTo: () => {
            feedActions.getFeed();
        }
    },

    getInitialState() {
        return {
            activePage: 0
        };
    },

    mixins: [
        Reflux.connect( feedStore, 'feed' )
    ],

    render() {
        document.title = 'Soapee - News Feed';

        return (

                { this.renderLoading() }
                { this.renderNewsFeed() }

        );
    },

    renderLoading() {
        if ( !(this.state.feed.length) ) {
            return <Spinner />;
        }
    },

    renderNewsFeed() {
        if ( this.state.feed.length ) {
            return (

                    { _.map( this.state.feed, this.renderFeedItem ) }
                    { this.paginator() }

            );
        }
    },

    renderFeedItem( feedItem ) {
        return (
            <FeedItem
                feedItem={feedItem}
                />
        );
    },

    paginator() {
        if ( feedStore.pagerVisible() ) {
            return (
                <Pager
                    total={feedStore.totalPages()}
                    current={this.state.activePage}
                    visiblePages={5}
                    onPageChanged={this.onPageChanged}
                    />
            );
        }
    },

    onPageChanged( page ) {
        this.setState( {
            activePage: page
        } );
        feedActions.getFeed( {
            page
        } );
    }

} );


text:
import moment from 'moment';
import React from 'react';
import Reflux from 'reflux';
import { Link, State, Navigation } from 'react-router';

import statusUpdateActions from 'actions/statusUpdate';

import statusUpdateStore from 'stores/statusUpdate';
import statusUpdateCommentStore from 'stores/statusUpdateComments';
import authStore from 'stores/auth';

import Commentable from 'components/commentable';
import ImageableCarousel from 'components/imageableCarousel';
import MarkedDisplay from 'components/markedDisplay';
import Spinner from 'components/spinner';
import UserAvatar from 'components/userAvatar';

export default React.createClass( {

    mixins: [
        Navigation,
        State,
        Reflux.connect( statusUpdateStore, 'statusUpdate' )
    ],

    componentDidMount() {
        statusUpdateActions.getStatusUpdate( this.getParams().id )
            .then( statusUpdateActions.getStatusUpdateComments );
    },

    render() {
        document.title = 'Soapee - Status Update';

        return (

<Link to="home">Home</Link>

Status Update

                { this.renderLoading() }
                { this.renderStatusUpdate() }

        );
    },

    renderLoading() {
        if ( !(this.state.statusUpdate) ) {
            return <Spinner />;
        }
    },

    renderStatusUpdate() {
        let {statusUpdate} = this.state;

        if ( statusUpdate ) {
            return (

                    <legend>Status Update</legend>

                            <UserAvatar
                                user={ statusUpdate.user }
                                />

                                    <Link to="userProfile" params={ { id: statusUpdate.user_id } }>{ statusUpdate.user.name }</Link>

                                     posted { moment( statusUpdate.created_at ).fromNow() }

                        <MarkedDisplay
                            content={ statusUpdate.update }
                            />

                    { statusUpdate.images.length > 0 &&

                                <legend>Photos</legend>

                                    <ImageableCarousel
                                        images={ statusUpdate.images}
                                        />

                    }

                    { authStore.isMyId( statusUpdate.user_id ) &&

                            <button className="btn btn-primary" onClick={this.editStatusUpdate}> Edit Status Update</button>

                    }

                    <legend>Comments</legend>

                    <Commentable
                        store={statusUpdateCommentStore}
                        />

            );
        }
    },

    editStatusUpdate() {
        this.transitionTo( 'status-update-edit', { id: this.state.statusUpdate.id } );
    }

} );


text:
import _ from 'lodash';
import React from 'react';
import Reflux from 'reflux';
import { Link } from 'react-router';

import meStore from 'stores/me';
import meActions from 'actions/me';

import RecipeListItem from 'components/recipeListItem';
import Spinner from 'components/spinner';

export default React.createClass( {

    mixins: [
        Reflux.connect( meStore, 'profile' )
    ],

    componentDidMount() {
        meActions.getMyRecipes();
    },

    render() {
        document.title = 'Soapee - My Recipes';

        return (

                <legend>My Recipes</legend>

                { this.renderLoading() }
                { this.renderRecipes() }

        );
    },

    renderLoading() {
        if ( !(this.state.profile.myRecipes) ) {
            return <Spinner />;
        }
    },

    renderRecipes() {
        let lengthRecipes = _.get(this.state.profile.myRecipes, 'length', 0);

        if ( lengthRecipes > 0 ) {
            return _( this.state.profile.myRecipes )
                .sortBy( 'created_at' )
                .reverse()
                .map( this.renderRecipe )
                .value();
        } else if ( lengthRecipes === 0 ) {
            return (

                    No recipes found. Use the <Link to="calculator">calculator</Link> to add recipes to your profile.

            );
        }
    },

    renderRecipe( recipe ) {
        return (
            <div key={ `recipe-${ recipe.id }` }>
                <RecipeListItem
                    recipe={recipe}
                    />

        );
    }

} );


text:
import React from 'react';
import Reflux from 'reflux';
import DocMeta from 'react-doc-meta';

import { Navigation } from 'react-router';

import calculatorStore from 'stores/calculator';
import recipeActions from 'actions/recipe';

import FormSaveRecipe from 'components/formSaveRecipe';
import Imageable from 'components/imageable';
import SapCalculator from 'components/sapCalculator';

export default React.createClass( {

    mixins: [
        Navigation,
        Reflux.connect( calculatorStore, 'recipe' )
    ],

    getInitialState() {
        return {
            saving: false
        };
    },

    componentDidMount() {
        document.title = 'Soapee - Lye Calculator';
    },

    render() {
        return (

                <DocMeta tags={ this.tags() } />
                <SapCalculator
                    recipe={ this.state.recipe }
                    />

                { this.state.recipe.countWeights() > 0 &&

                        <FormSaveRecipe
                            recipe={ this.state.recipe }
                            buttonPrint={ true }
                            buttonReset={ true }
                            buttonCaptionSave={ this.saveCaption() }
                            buttonDisabledSave={ this.state.saving }
                            onSave={ this.saveRecipe }
                            onPrint={ this.printRecipe }
                            onPrintPreview={ this.printPreviewRecipe }
                            onReset={ this.resetRecipe }
                            />

                }

        );
    },

    startImageUploadHookFn( fnToStartUploads ) {
        this.startUploads = fnToStartUploads;
    },

    saveCaption() {
        return this.state.saving ? 'Saving Recipe' : 'Save Recipe';
    },

    saveRecipe() {
        function uploadImages() {
            this.startUploads( this.newRecipe.id );
        }

        this.setState( {
            saving: true
        } );

        return recipeActions.createRecipe( this.state.recipe )
            .then( recipe => this.newRecipe = recipe )
            .then( this.showRecipe.bind(this) )
            .finally(() => this.setState({
                saving: false
            }));
    },

    showRecipe() {
        this.resetRecipe();
        this.transitionTo( 'recipe', { id: this.newRecipe.id } );
    },

    printRecipe() {
        this.replaceWith( 'print' );
    },

    printPreviewRecipe() {
        this.transitionTo( 'print', null, { preview: true } );
    },

    resetRecipe() {
        recipeActions.resetRecipe();
    },

    tags() {
        let description = 'Soapee Lye Calculator';

        return [
            {name: 'description', content: description},
            {name: 'twitter:card', content: description},
            {name: 'twitter:title', content: description},
            {property: 'og:title', content: description}
        ];
    }

} );


text:
import React from 'react';
import Reflux from 'reflux';
import DocMeta from 'react-doc-meta';
import cx from 'classnames';
import { Link, Navigation, State } from 'react-router';

import recipeStore from 'stores/recipe';
import authStore from 'stores/auth';
import recipeComments from 'stores/recipeComments';
import recipeJournals from 'stores/recipeJournals';

import recipeActions from 'actions/recipe';
import meActions from 'actions/me';

import Spinner from 'components/spinner';
import RecipeBreakdown from 'components/recipeBreakdown';
import RecipeJournals from 'components/recipeJournals';
import RecipeTotals from 'components/recipeTotals';
import RecipeFattyAcids from 'components/recipeFattyAcids';
import RecipeProperties from 'components/recipeProperties';
import FacebookComments from 'components/facebookComments';
import BootstrapModalLink from 'components/bootstrapModalLink';
import UserAvatar from 'components/userAvatar';
import ButtonFBLike from 'components/buttonFBLike';
import ButtonGPlusLike from 'components/buttonGPlusLike';
import MarkedDisplay from 'components/markedDisplay';
import Commentable from 'components/commentable';
import ImageableCarousel from 'components/imageableCarousel';

import SignupOrLoginToSaveRecipe from 'modals/signupOrLoginToSaveRecipe';

export default React.createClass( {

    statics: {
        willTransitionTo: function ( transition, params ) {
            recipeActions.getRecipeById( params.id )
                .tap( recipeActions.getRecipeComments )
                .tap( recipeActions.getRecipeJournals );
        }
    },

    mixins: [
        State,
        Navigation,
        Reflux.connect( recipeStore, 'recipe' ),
        Reflux.connect( authStore, 'auth' ),
        Reflux.connect( recipeComments, 'comments' ),
        Reflux.connect( recipeJournals, 'journals' )
    ],

    render() {
        if ( this.state.recipe.error ) {
            return (

                    { this.renderError() }

            );
        } else {
            return (

                    <DocMeta tags={ this.tags() }/>
                    { this.renderLoading() }
                    { this.renderRecipe() }

            );
        }
    },

    renderRecipe() {
        let recipeName;
        let recipeDescription;
        let recipeNotes;

        let notesActive;
        let commentsActive;

        if ( this.pageIsForRequestedRecipe() ) {
            recipeName = this.state.recipe.getModelValue( 'name' );
            recipeDescription = this.state.recipe.getModelValue( 'description' );
            recipeNotes = this.state.recipe.getModelValue( 'notes' );

            notesActive = recipeNotes;
            commentsActive = !notesActive;

            document.title = `Soapee - ${this.state.recipe.getModelValue( 'name' )}`;

            return (

<Link to="home">Home</Link>

<Link to="recipes">Recipes</Link>

{recipeName}

                    <legend>
{recipeName}
</legend>
                    { recipeDescription &&
<MarkedDisplay content={ recipeDescription }/>
 }

Oils

                                <RecipeBreakdown
                                    recipe={ this.state.recipe }
                                    withOilLinks={true}
                                    />

                            <Link to="userProfile" params={ { id: this.state.recipe.getModelValue( 'user_id' ) } }>
                                <UserAvatar
                                    user={ this.state.recipe.getModelValue( 'user' ) }
                                    showTooltip={ true }
                                    />
                            </Link>
                            { this.state.recipe.isPrivate() &&

Private
Recipe

                            }
                            { this.state.recipe.isPrivateFriends() &&

Private
Friends
Recipe

                            }

                                <ButtonFBLike
                                    url={ window.location }
                                    />
                                <ButtonGPlusLike
                                    url={ window.location }
                                    />

Total Weights

                                <RecipeTotals
                                    recipe={ this.state.recipe }
                                    />

Recipe Properties

                                <RecipeProperties
                                    recipe={ this.state.recipe }
                                    withRange={true}
                                    showTooltips={true}
                                    />

Fatty Acids %

                                <RecipeFattyAcids
                                    recipe={ this.state.recipe }
                                    />

                    { this.state.recipe.recipe.images.length > 0 &&

                                <legend>Photos</legend>

                                    <ImageableCarousel
                                        images={ this.state.recipe.recipe.images }
                                        />

                    }

                    <legend></legend>
                    { this.renderActionButtons() }

                            { recipeNotes &&

                                    Directions

                            }

User Comments {this.countComments()}

Facebook Comments

Recipe Journals {this.countJournals()}

                            { recipeNotes &&

                                    <MarkedDisplay content={ recipeNotes }/>

                            }

                                <Commentable
                                    store={ recipeComments }
                                    />

                                <FacebookComments
                                    url={ window.location }
                                    />

                                <RecipeJournals
                                    recipe={ this.state.recipe.recipe }
                                    />

            );
        }
    },

    countComments() {
        let count = recipeComments.count();

        if ( count ) {
            return ({ count });
        }
    },

    countJournals() {
        let count = recipeJournals.count;

        if ( count ) {
            return ({ count });
        }
    },

    renderLoading() {
        if ( !(this.pageIsForRequestedRecipe()) ) {
            return <Spinner />;
        }
    },

    renderActionButtons() {
        let printButton;
        let previewButton;
        let editButton;
        let addToFavouritesButton;
        let deleteButton;

        printButton = <Link to="printRecipe" params={ { id: this.getParams().id } } className="btn btn-primary"> Print Recipe</Link>;
        previewButton = <Link to="printRecipe" params={ { id: this.getParams().id } } query={ { preview: true } } className="btn btn-primary"> Print Preview</Link>;
        editButton = <Link to="editRecipe" params={ { id: this.getParams().id } } className="btn btn-primary"> Edit Recipe</Link>;

        if ( authStore.isAuthenticated() ) {
            if ( !(authStore.isMyId( this.state.recipe.getModelValue( 'user_id' ) ) ) ) {
                addToFavouritesButton = <button className="btn btn-primary" onClick={ this.addToFavourites }> Add to Favourites</button>;
                editButton = <Link to="editRecipe" params={ { id: this.getParams().id } } className="btn btn-primary"> Copy and Edit Recipe</Link>;
            } else {
                deleteButton = <button className="btn btn-warning" onClick={ this.deleteRecipe }> Delete Recipe</button>;
            }
        } else {
            addToFavouritesButton = (
                <BootstrapModalLink
                    elementToClick={<button className="btn btn-primary"> Add to Favourites</button>}
                    modal={SignupOrLoginToSaveRecipe}
                    action="add recipe to favourites"
                    />
            );

        }

        return (

                { printButton }
                { previewButton }
                { editButton }
                { addToFavouritesButton }
                { deleteButton }

        );
    },

    addToFavourites() {
        function success() {
            $.bootstrapGrowl( 'Recipe Added to your Favourites', { type: 'warning', delay: 5000 } );
        }

        meActions.addRecipeToFavourites( this.state.recipe.recipe )
            .then( success );
    },

    deleteRecipe() {
        if ( confirm( 'Delete this recipe?' ) ) {
            recipeActions.deleteRecipe( this.state.recipe )
                .then( () => this.transitionTo( 'my-recipes' ) );
        }
    },

    pageIsForRequestedRecipe() {
        let requested = Number( this.getParams().id );
        let got = Number( this.state.recipe.getModelValue( 'id' ) );

        return requested === got;
    },

    renderError() {
        let error = this.state.recipe.error;
        let message;

        if ( error.errorType === 'NotAuthorisedError' ) {
            message = (

Private Recipe

This recipe has been marked as private and can only be viewed by the owner.

Please <Link to="login">login</Link> to view this recipe if your are its author.

            );
        } else if ( error.errorType === 'RecordNotFound' ) {
            message = (

Recipe not Found

Could not find this recipe. It might have been deleted by its owner.

            );
        } else {
            message = (

Unexpected Error

Oh dear! Not sure what happened there.

                        I would be most grateful if you would report this error to either the
                        Reddit support group or to our
                        page on Facebook.

            );
        }

        return message;
    },

    tags() {
        let description = `Soapee Recipe - ${this.state.recipe.getModelValue( 'name' )}`;

        return [
            { name: 'description', content: description },
            { name: 'twitter:card', content: description },
            { name: 'twitter:title', content: description },
            { property: 'og:title', content: description }
        ];
    }

} );


text:
import React from 'react';
import Reflux from 'reflux';

import {RouteHandler} from 'react-router';

import analyticsStore from 'stores/analytics';

import NavBar from 'components/navbar';
import Footer from 'components/footer';

export default React.createClass( {

    mixins: [
        Reflux.connect( analyticsStore )
    ],

    render() {
        return (

                <NavBar />

                    <RouteHandler />

        );
    }

} );


text:
import React from 'react';
import Reflux from 'reflux';
import { Navigation, State } from 'react-router';

import recipeStore from 'stores/recipe';
import recipeActions from 'actions/recipe';

import RecipePrintArea from 'components/recipePrintArea';

export default React.createClass( {

    mixins: [
        Navigation,
        State,
        Reflux.connect( recipeStore, 'recipe' )
    ],

    componentDidMount() {
        let recipe;

        function print() {
            return window.print();
        }

        function redirectBack() {
            this.replaceWith( 'recipe', { id: this.getParams().id } );
        }

        recipe = recipeActions.getRecipeById( this.getParams().id );

        if ( this.getQuery().preview !== 'true' ) {
            recipe
                .with( this )
                .then( print )
                .then( redirectBack );
        }
    },

    render() {
        return (
            <div id="recipe-print">
                <RecipePrintArea
                    recipe={ this.state.recipe }
                    />
            </div>
        );
    }

} );


text:
import React from 'react';
import Reflux from 'reflux';
import { Link, State } from 'react-router';

import recipeActions from 'actions/recipe';

import recipeStore from 'stores/recipe';
import recipeJournalStore from 'stores/recipeJournal';
import recipeJournalCommentStore from 'stores/journalComments';

import Commentable from 'components/commentable';
import RecipeJournalItem from 'components/recipeJournalItem';
import Spinner from 'components/spinner';

export default React.createClass( {

    mixins: [
        State,
        Reflux.connect( recipeStore, 'recipe' ),
        Reflux.connect( recipeJournalStore, 'journal' )
    ],

    componentDidMount() {
        recipeJournalStore.reset();

        recipeActions.getRecipeById( this.getParams().recipeId )
            .tap( recipe => recipeActions.getRecipeJournal( recipe, { id: this.getParams().journalId } ) )
            .then( recipe => recipeActions.getRecipeJournalComments( recipe, { id: this.getParams().journalId } ) );
    },

    render() {
        let recipe = this.state.recipe;

        document.title = 'Soapee - Journal';

        return (

<Link to="home">Home</Link>

<Link to="recipes">Recipes</Link>

                    { Number(recipe.getModelValue( 'id' )) > 0 &&
<Link to="recipe" params={ { id: recipe.getModelValue( 'id' ) } }>{ recipe.getModelValue( 'name' ) }</Link>
 }

Viewing Recipe Journal

                { this.renderLoading() }
                { this.renderJournal() }

        );
    },

    renderJournal() {
        let journal = this.state.journal;
        let recipe = this.state.recipe.recipe;

        if ( journal ) {
            return (

                    <legend>Recipe Journal Entry</legend>
                    <RecipeJournalItem
                        recipe={ recipe }
                        recipeJournal={ journal }
                        imagesType="carousel"
                        state="view"
                        />

                    <legend>Comments</legend>

                    <Commentable
                        store={recipeJournalCommentStore}
                        />

            );
        }
    },

    renderLoading() {
        if ( !(this.state.journal) ) {
            return (
                <Spinner />
            );
        }
    }

} );


text:
import _ from 'lodash';
import React from 'react';
import Reflux from 'reflux';

import meFriendsRecipesStore from 'stores/myFriendsRecipes';
import meActions from 'actions/me';

import RecipeListItem from 'components/recipeListItem';
import Spinner from 'components/spinner';

export default React.createClass( {

    mixins: [
        Reflux.connect( meFriendsRecipesStore, 'recipes' )
    ],

    componentDidMount() {
        meActions.getMyFriendsRecipes();
    },

    render() {
        document.title = 'Soapee - My Friend\'s Recipes';

        return (

                <legend>My Friend's Recipes</legend>

                { this.renderLoading() }
                { this.renderRecipes() }

        );
    },

    renderLoading() {
        if ( !(this.state.recipes) ) {
            return <Spinner />;
        }
    },

    renderRecipes() {
        let lengthRecipes = _.get(this.state.recipes, 'length', 0);

        if ( lengthRecipes > 0 ) {
            return _.map( this.state.recipes, this.renderRecipe );
        } else if ( lengthRecipes === 0 ) {
            return (

                    No recipes found.

            );
        }
    },

    renderRecipe( recipe ) {
        return (
            <div key={ `recipe-${ recipe.id }` }>
                <RecipeListItem
                    showUser={ true }
                    recipe={recipe}
                    />

        );
    }

} );


text:
import _ from 'lodash';
import React from 'react';
import Reflux from 'reflux';
import DocMeta from 'react-doc-meta';
import Pager from 'react-pager';

import recipeActions from 'actions/recipe';
import recipesStore from 'stores/recipes';

import RecipeListItem from 'components/recipeListItem';
import Spinner from 'components/spinner';

export default React.createClass( {

    mixins: [
        Reflux.connect( recipesStore, 'recipes' )
    ],

    getInitialState() {
        return {
            activePage: 0
        };
    },

    componentDidMount() {
        document.title = 'Soapee - Recipes';
        recipeActions.getRecipes();
    },

    render() {
        return (

                <DocMeta tags={ this.tags() } />

Latest Public Soap Recipes

                { this.renderLoading() }
                { this.renderRecipes() }

                { this.paginator() }

        );
    },

    renderLoading() {
        if ( !(this.state.recipes.length > 0) ) {
            return <Spinner />;
        }
    },

    renderRecipes() {
        if ( this.state.recipes.length > 0 ) {
            return _( this.state.recipes )
                .sortBy( 'created_at' )
                .reverse()
                .map( this.renderRecipe )
                .value();
        }
    },

    paginator() {
        if ( this.state.recipes.length > 0 ) {
            return (
                <Pager
                    total={ recipesStore.totalPages() }
                    current={this.state.activePage}
                    visiblePages={5}
                    onPageChanged={this.onPageChanged}
                    />
            );
        }
    },

    onPageChanged: function( page ) {
        this.setState( {
            activePage: page
        } );

        recipeActions.getRecipes( {
            page
        } );
    },

    renderRecipe( recipe ) {
        return (
            <div key={ `recipe-list-item-${recipe.id}` }>
                <RecipeListItem
                    recipe={recipe}
                    showUser={ true }
                    />

        );
    },

    tags() {
        let description = 'Soapee Community Soap Recipes';

        return [
            {name: 'description', content: description},
            {name: 'twitter:card', content: description},
            {name: 'twitter:title', content: description},
            {property: 'og:title', content: description}
        ];
    }

} );


text:
import _ from 'lodash';
import React from 'react/addons';
import Reflux from 'reflux';
import { Link, Navigation, State } from 'react-router';

import recipeActions from 'actions/recipe';
import formLinkHandlers from 'mixins/formLinkHandlers';

import  recipeStore from 'stores/recipe';
import  recipeJournalStore from 'stores/recipeJournal';

import Imageable from 'components/imageable';
import ImageableEdit from 'components/imageableEdit';
import MarkdownEditor from 'components/markdownEditor';
import Spinner from 'components/spinner';

export default React.createClass( {

    mixins: [
        Navigation,
        State,
        formLinkHandlers,
        Reflux.connect( recipeStore, 'recipe' ),
        Reflux.connect( recipeJournalStore, 'journal' )
    ],

    componentDidMount() {
        recipeJournalStore.reset();

        recipeActions.getRecipeById( this.getParams().recipeId )
            .tap( recipe => recipeActions.getRecipeJournal( recipe, { id: this.getParams().journalId } ) )
            .then( recipe => recipeActions.getRecipeJournalComments( recipe, { id: this.getParams().journalId } ) );
    },

    render() {
        let recipe = this.state.recipe;
        document.title = 'Soapee - Editing Journal';

        return (

<Link to="home">Home</Link>

<Link to="recipes">Recipes</Link>

                    { Number( recipe.getModelValue( 'id' ) ) > 0 &&
<Link to="recipe" params={ { id: recipe.getModelValue( 'id' ) } }>{ recipe.getModelValue( 'name' ) }</Link>
 }

Editing Recipe Journal

                { this.renderLoading() }
                { this.renderJournalEditForm() }

        );
    },

    renderJournalEditForm() {
        if ( this.state.journal ) {

            return (

                    <MarkdownEditor
                        placeholder="Add your journal...."
                        valueLink={ this.linkComponentState( this, 'journal.journal' ) }
                        rows={ 3 }
                        />

                    { this.state.errors &&

                            { this.state.errors }

                    }

                    { _.get( this.state, 'journal.images.length' ) > 0 &&

                                <legend>Delete Photos?</legend>
                                <ImageableEdit
                                    images={ this.state.journal.images }
                                    />

                    }

                        <button className="btn btn-primary"
                                onClick={ this.updateJournal }
                                disabled={ this.state.saving }
                            >
                             Save
                        </button>
                        <button className="btn btn-primary"
                                onClick={ this.clearAndRedirect }
                            >
                             Cancel
                        </button>
                        <button className="btn btn-warning"
                                onClick={ this.deleteJournal }
                            >
                             Delete
                        </button>

            );

        }
    },

    renderLoading() {
        if ( !(this.state.journal) ) {
            return (
                <Spinner />
            );
        }
    },

    startImageUploadHookFn( startFn ) {
        this.startImageUpload = startFn;
    },

    updateJournal() {
        this.setState( {
            saving: true
        } );

        recipeActions.updateRecipeJournal( this.state.recipe.recipe, this.state.journal )
            .then( () => this.startImageUpload( this.state.journal.id ) );
    },

    deleteJournal() {
        if ( confirm( 'Delete this recipe journal?' ) ) {
            recipeActions.deleteRecipeJournal( this.state.recipe.recipe, this.state.journal )
                .then( () => this.transitionTo( 'recipe', { id: this.state.recipe.getModelValue( 'id' ) } ) );
        }
    },

    clearAndRedirect() {
        this.setState( {
            saving: false,
            errors: null
        } );

        this.transitionTo( 'recipe-journal', {
            recipeId: this.state.recipe.getModelValue( 'id' ),
            journalId: this.state.journal.id
        } );
    }

} );


text:
import _ from 'lodash';
import React from 'react';
import Reflux from 'reflux';
import { Link } from 'react-router';

import meStore from 'stores/me';
import meActions from 'actions/me';

import RecipeListItem from 'components/recipeListItem';
import Spinner from 'components/spinner';

export default React.createClass( {

    mixins: [
        Reflux.connect( meStore, 'profile' )
    ],

    componentDidMount() {
        meActions.getMyFavouriteRecipes();
    },

    render() {
        document.title = 'Soapee - Saved Recipes';

        return (

                <legend>My Favourite Recipes</legend>

                { this.renderLoading() }
                { this.renderRecipes() }

        );
    },

    renderLoading() {
        if ( !(this.state.profile.myFavouriteRecipes) ) {
            return <Spinner />;
        }
    },

    renderRecipes() {
        let lengthRecipes = _.get(this.state.profile.myFavouriteRecipes, 'length', 0);

        if ( lengthRecipes > 0 ) {
            return _( this.state.profile.myFavouriteRecipes )
                .sortBy( 'created_at' )
                .reverse()
                .map( this.renderRecipe )
                .value();
        } else if ( lengthRecipes === 0 ) {
            return (

                    No Recipes found. Add other member's <Link to="recipes">recipes</Link> to your favourites to view them here.

            );
        }
    },

    renderRecipe( recipe ) {
        return (
            <div key={ `recipe-${ recipe.id }` }>
                <RecipeListItem
                    recipe={recipe}
                    showUser={ true }
                    />

        );
    }

} );


text:
import _ from 'lodash';
import React from 'react';
import Reflux from 'reflux';
import DocMeta from 'react-doc-meta';
import { Link, State } from 'react-router';

import oilActions from 'actions/oil';

import oilStore from 'stores/oil';
import oilsStore from 'stores/oils';
import calculatorStore from 'stores/calculator';
import oilComments from 'stores/oilComments';

import Spinner from 'components/spinner';
import FacebookComments from 'components/facebookComments';
import ButtonFBLike from 'components/buttonFBLike';
import ButtonGPlusLike from 'components/buttonGPlusLike';
import Commentable from 'components/commentable';
import RecipesLinkTable from 'components/recipesLinkTable';

export default React.createClass( {

    statics: {
        willTransitionTo: function ( transition, params ) {
            oilActions.getOilById( params.id );
        }
    },

    mixins: [
        State,
        Reflux.connect( oilStore, 'oil' ),
        Reflux.connect( oilComments, 'comments' )
    ],

    render() {
        return (

                { this.renderLoading() }
                { this.renderOil() }

        );
    },

    renderOil() {
        let oilName;

        if ( this.pageIsForRequestedOil() ) {
            oilName = this.state.oil.name;

            document.title = `Soapee - ${ oilName }`;

            return (

                    <DocMeta tags={ this.tags() } />

<Link to="home">Home</Link>

<Link to="oils">Oils</Link>

{oilName}

                    <legend>
{oilName}
</legend>

Fatty Acids

                                { this.renderFattyAcids() }

Oil Properties

                                { this.renderProperties() }

Saponification Values

                                { this.renderSaponification() }

                                <ButtonFBLike
                                    url={ window.location }
                                    />
                                <ButtonGPlusLike
                                    url={ window.location }
                                    />

Used in Recipes

User Comments {this.countComments()}

Facebook Comments

                                    { this.renderInRecipes() }

                                    <Commentable
                                        store={ oilComments }
                                        />

                                    <FacebookComments
                                        url={ window.location }
                                        />

            );
        }
    },

    renderSaponification() {
        let oil = this.state.oil;

        return (

                        KOH
                        {oil.sap}

                        NaOH
                        {calculatorStore.sapForNaOh(oil)}

        );
    },

    renderFattyAcids() {
        let oil = this.state.oil;
        let labels = {
            saturated: 'Saturated',
            monoSaturated: 'Mono-unsaturated',
            polySaturated: 'Poly-unsaturated'
        };
        let breakdowns = _.transform( oilsStore.getAllFats(), ( output, fat ) => {
            let breakdown = oil.breakdown[ fat ];

            if ( breakdown ) {
                output.push(

                        {_.capitalize(fat)}
                        {breakdown}%

                );
            }
        }, [] );
        let saturations =  _( oil.saturation )
            .pick( 'saturated', 'monoSaturated', 'polySaturated' )
            .map( ( satType, saturation ) => {
                return (

                        { labels[ saturation ] }:
                        { satType }%

                );
            } )
            .value();

        let ratios = `${oil.saturation.saturated} : ${ 100 - oil.saturation.saturated }`;
        let ratiosRow = (

                Saturation Ratio
                { ratios }

        );

        return (

                    { breakdowns }
                    { this.gap() }
                    { saturations }
                    { this.gap() }
                    { ratiosRow }

        );
    },

    renderProperties() {
        let oil = this.state.oil;
        let properties;

        function render( property ) {
            return (

                    {_.capitalize( property )}
                    {oil.properties[ property ]}%

            );
        }

        properties = _( oil.properties )
            .keys()
            .sort()
            .map( render, this )
            .value();

        return (

                    { properties }

        );
    },

    renderInRecipes() {
        let oil = this.state.oil;

        if ( oil.recipes && oil.recipes.length ) {
            return (
                <RecipesLinkTable
                    recipes={ oil.recipes }
                    />
            );
        } else {
            return (

Not used in any recipes.

            );
        }

    },

    countComments() {
        let count = oilComments.count();

        if ( count ) {
            return ({ count });
        }
    },

    renderLoading() {
        if ( !(this.pageIsForRequestedOil()) ) {
            return <Spinner />;
        }
    },

    pageIsForRequestedOil() {
        let requested = Number( this.getParams().id );
        let got = Number( _.get( this.state.oil, 'id' ) );

        return requested === got;
    },

    gap() {
        return (

        );
    },

    tags() {
        let description = `Soapee Oil - ${ this.state.oil.name }`;

        return [
            {name: 'description', content: description},
            {name: 'twitter:card', content: description},
            {name: 'twitter:title', content: description},
            {property: 'og:title', content: description}
        ];
    }

} );


text:
import React from 'react/addons';
import DocMeta from 'react-doc-meta';

import FacebookComments from 'components/facebookComments';

export default React.createClass( {

    render() {
        document.title = 'Soapee - Resources';

        return (

                <DocMeta tags={ this.tags() } />

Soap Making Resources

These are some resources from around the net that helped in my soap making journey. Please add any additional suggestions for this page in the comments box bellow.

Websites

Soap Making on Reddit

Soap Making group on Facebook

Soap Making Forum

Modern Soap Making Blog

Videos

Books

Products

Facebook Comments

                                <FacebookComments
                                    url={ window.location }
                                    />

        );
    },

    tags() {
        let description = `Soapee Soap Making Resources`;

        return [
            {name: 'description', content: description},
            {name: 'twitter:card', content: description},
            {name: 'twitter:title', content: description},
            {property: 'og:title', content: description}
        ];
    }

} );


text:
import _ from 'lodash';
import React from 'react';
import Reflux from 'reflux';
import { Link } from 'react-router';

import myCommentsStore from 'stores/myComments';
import meActions from 'actions/me';

import Comment from 'components/comment';
import Spinner from 'components/spinner';

export default React.createClass( {

    mixins: [
        Reflux.connect( myCommentsStore, 'comments' )
    ],

    componentDidMount() {
        meActions.getComments();
    },

    render() {

        return (

                <legend>My Comments</legend>
                { this.renderLoading() }
                { this.renderComments() }

        );

    },

    renderLoading() {
        if ( !(this.state.comments) ) {
            return <Spinner />;
        }
    },

    renderComments() {
        if ( this.state.comments ) {
            return (

                    { _.map( this.state.comments, this.renderComment, this ) }

            );
        }
    },

    renderComment( comment ) {
        let links;
        let route;

        route = {
            oils: 'oil',
            recipes: 'recipe',
            status_updates: getStatusUpdateLinks,
            recipe_journals: getRecipeJournalsLinks
        }[ comment.commentable_type ];

        if ( _.isFunction( route ) ) {
            links = route();
        } else {
            links = [ <Link to={ route } params={ { id: comment.commentable_id } }>in { comment.commentable_type }</Link> ];
        }

        return (
            <div key={ `comment-${ comment.id }` }>
                <Comment
                    comment={ comment }
                    links={ links }
                    />

        );

        function getStatusUpdateLinks() {
            return [ <Link to="status-update" params={ { id: comment.commentable_id } }>in status update</Link> ];
        }

        function getRecipeJournalsLinks() {
            return [ <Link to="recipe-journal" params={ { recipeId: comment.commentable.recipe_id, journalId: comment.commentable_id } }>in recipe journal</Link> ];
        }
    }

} );


text:
var utils = require('shipit-utils');
var path = require('path');
var moment = require('moment');
var chalk = require('chalk');
var _ = require('lodash');
var util = require('util');
var Promise = require('bluebird');

/**
 * Update task.
 * - Set previous release.
 * - Set previous revision.
 * - Create and define release path.
 * - Copy previous release (for faster rsync)
 * - Set current revision and write REVISION file.
 * - Remote copy project.
 */

module.exports = function (gruntOrShipit) {
    utils.registerTask(gruntOrShipit, 'deploy:update-local', task);

    function task() {
        var shipit = utils.getShipit(gruntOrShipit);
        _.assign(shipit.constructor.prototype, require('shipit-deploy/lib/shipit'));

        return setPreviousRelease()
            .then(setPreviousRevision)
            .then(createReleasePath)
            .then(copyPreviousRelease)
            .then(remoteCopy)
            .then(function () {
                shipit.emit('updated');
            });

        /**
         * Copy previous release to release dir.
         */

        function copyPreviousRelease() {
            if (!shipit.previousRelease) {
                return Promise.resolve();
            }
            return shipit.remote(util.format('cp -a %s/. %s', path.join(shipit.releasesPath, shipit.previousRelease), shipit.releasePath));
        }

        /**
         * Create and define release path.
         */

        function createReleasePath() {
            shipit.releaseDirname = moment.utc().format('YYYYMMDDHHmmss');
            shipit.releasePath = path.join(shipit.releasesPath, shipit.releaseDirname);

            shipit.log('Create release path "%s"', shipit.releasePath);
            return shipit.remote('mkdir -p ' + shipit.releasePath)
                .then(function () {
                    shipit.log(chalk.green('Release path created.'));
                });
        }

        /**
         * Remote copy project.
         */

        function remoteCopy() {
            var uploadDirPath = path.resolve(shipit.config.workspace, shipit.config.dirToCopy || '');

            shipit.log('Copy project to remote servers.');
            return shipit.remoteCopy(uploadDirPath + '/', shipit.releasePath, {rsync: '--del'})
                .then(function () {
                    shipit.log(chalk.green('Finished copy.'));
                });
        }

        /**
         * Set shipit.previousRevision from remote REVISION file.
         */

        function setPreviousRevision() {
            shipit.previousRevision = null;

            if (!shipit.previousRelease) {
                return Promise.resolve();
            }

            return shipit.getRevision(shipit.previousRelease)
                .then(function(revision) {

                    if (revision) {
                        shipit.log(chalk.green('Previous revision found.'));
                        shipit.previousRevision = revision;
                    }
                });
        }

        /**
         * Set shipit.previousRelease.
         */

        function setPreviousRelease() {
            shipit.previousRelease = null;
            return shipit.getCurrentReleaseDirname()
                .then(function(currentReleasseDirname) {
                    if (currentReleasseDirname) {
                        shipit.log(chalk.green('Previous release found.'));
                        shipit.previousRelease = currentReleasseDirname;
                    }
                });
        }

    }
};


text:
var config = {
    default: {
        workspace: './build',
        deployTo: '/var/www/soapee.com/ui/',
        rsync: [ '--del' ],
        keepReleases: 3,
        deleteOnRollback: false
    },

    production: {
        servers: [ {
            host: '176.58.125.89',
            user: 'nazar'
        } ],
        key: '/home/vagrant/files/deploy/keys/production'
    }
};

module.exports.config = config;

module.exports.init = function ( shipit ) {
    require( './deploy' )( shipit );

    shipit.initConfig( config );
};


text:
var utils = require( 'shipit-utils' );

module.exports = function ( gruntOrShipit ) {
    require( 'shipit-deploy' )( gruntOrShipit );

    require( './update' )( gruntOrShipit );

    utils.registerTask( gruntOrShipit, 'deploy-local', [
        'deploy:init',
        'deploy:update-local',
        'deploy:publish',
        'deploy:clean'
    ] );
};


text:
# !/usr/bin/env bash

# fix putty ctrl+left right arrow navigation

echo 'tput smkx' >> /home/vagrant/.profile


text:
#!/usr/bin/env bash

# install dependencies
sudo apt-get update
sudo apt-get install -y build-essential

# install node via nvm -  https://github.com/creationix/nvm
echo "Installing nvm..."
curl https://raw.githubusercontent.com/creationix/nvm/v0.25.4/install.sh | bash

echo "source /home/vagrant/.nvm/nvm.sh" >> /home/vagrant/.profile
source /home/vagrant/.profile

nvm install 0.12.5
nvm alias default 0.12.5

# install global npm packages

npm install -g gulp@3.9.0
npm install -g shipit-cli@1.3.0


text:
# Development Setup

## Virtual Development using Vagrant

[Vagrant](https://www.vagrantup.com) provides easy to configure, reproducible, and portable work environments built on top of
industry-standard technology and controlled by a single consistent workflow to help maximize the productivity and flexibility of you and your team.

## Setting up Vagrant

### 1. Download and Install Vagrant

Head to the Vagrant [downloads](http://www.vagrantup.com/downloads) and download the correct installation package for your OS

### 2. Download and Install VirtualBox

The project [Vagrantfile](../Vagrantfile) is configured for VirtualBox, which can be [downloaded here](https://www.virtualbox.org/wiki/Downloads).
Please ensure that the Host Only network option is selected during VirtualBox installation.

### 3. Download a Vagrant Base Box

Install the local base box using the following command:

```
vagrant box add bento/debian-7.8-i386
```

When asked, choose the virtualbox variant.

### 4. Provision the Local Development VM

Once **bento/debian-7.8-i386** has been downloaded open a command prompt at the project folder root and issue the following command:

```
vagrant up
```

This should clone the base box into a project specific box. The Vagrant file should also provision all pre-requisites (i.e. node, npm, build tools etc). Once complete, the box should be
accessible on address **192.168.30.20**. [PuTTY](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html) or [SmarTTY](http://smartty.sysprogs.com/)
can be used to SSH into 192.168.30.20 using the username/password of vagrant/vagrant.

### 5. Setup File Synchronization

Vagrant allows using your favourite IDE/Text Editor on your OS. These files are then synced on the Vagrant box, which is configured to build and
serve the web application. During development, for example, the web application is accessible on http://192.168.30.20:8080.

Vagrant VMs are almost perfect apart from the default Shared Folder mechanism. In VirtualBox, the vboxfs shared folder has a number of shortcomings:

#### Lacks symbolic link support

This can be hacked using:

```
  config.vm.provider "virtualbox" do |vb|
      vb.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/vagrant", "1"]
  end
```

Additionally, if using Windows, the non Admin user must be provided with [create symbolic link permissions](http://superuser.com/questions/124679/how-do-i-create-a-link-in-windows-7-home-premium-as-a-regular-user?answertab=votes#125981).

The above "works" but is less than ideal

#### Does not expose file changes

This is a critical issue as LiveReload or WebPack's Hot Reload never detects file changes. Any files in the /vagrant share will not be reloaded on file change.

#### Workaround 1

Use an alternative sync method such as RSYNC or [Vagrant Unison](https://github.com/mrdavidlaing/vagrant-unison)

#### Workaround 2

Configure your IDE/Text Editor to remote sync all files on change. WebStorm, for example, provides a facility where project files can be synced using SFTP.

### 6. Synchronize Project Files to Vagrant Box

Typically I create a **files** folder inside the vagrant home (/home/vagrant) folder and configure project files to be synchronized there.

Once synchronized, cd into ~/files and install all npm dependencies using:

```
npm install
```

Once all node dependencies are installed, start the development server using:

```
gulp
```

The development web application should now be accessible on [http://192.168.30.20:8080](http://192.168.30.20:8080).


text:
# Background

Operating systems have traditionally assumed that storage components are either
working or have failed and must be replaced. Most code has been developed using
the synchronous I/O model where operations wait until they either receive a
successful result or an error. This model is not suitable for complex storage
environments involving many devices or, especially, networks where failures are
more common and might be transient. Additionally, Linux, FreeBSD and macOS have
all had serious NFS client bugs and design flaws which significantly amplified
the damage from even a momentary failure or overload.

Unfortunately many programs have non-obvious triggers which cause them to scan
all mounted filesystems, causing them to hang as soon as any mounted filesystem
stops responding. In the case of NFS home directories the user experience is
especially bad because most desktop environments will completely hang trying to
access configuration files from the user's home directory, making the system
unusable.

Finally, very few applications have the complicated code required to report when a
storage request has blocked for a long period of time which makes it hard for a
system administrator to proactively correct the problem. In some cases a reboot
may be required but in many cases the fallout from a temporary outage can be
significantly reduced by using `umount -f` or, in certain situations on Linux,
`umount -f -l`, and remounting the filesystem so any new process will be
completely unaffected.

## What `mount_status_monitor` does

`mount_status_monitor` provides the missing notification solution for imperfect
storage. It's a simple daemon which periodically checks every mounted filesystem
using an asynchronous check with a timeout so it can report soft failures caused
by unresponsive storage as well as hard errors.

After each run is complete it will send a message to syslog:

    Checked 5 mounts; 0 are dead

Optionally, the [Prometheus push-gateway](https://prometheus.io/docs/instrumenting/pushing/)
will receive two metrics (`total_mountpoints` and `dead_mountpoints`) with the
same information for alerting and correlation purposes.

When a mount test fails the mountpoint will be sent to syslog and stderr:

    Mount failed health-check: /Volumes/TestSSHFS

There are several ways to simulate failures for testing. The easiest is to use a
user-mode filesystem such as sshfs, s3fs, etc. and use `kill -STOP` to freeze
the FUSE process long enough to trigger the unresponsive mount failure. For more
involved testing or if you are also evaluating system tuning options you can use
iptables to simulate packet loss or hard failure of an NFS server.

## Installation

Compiling the code requires a working [Rust](https://www.rust-lang.org) toolchain:

    cargo build --release

A Docker image is provided for testing basic functionality:

    docker build -t mountstatus . && docker run -it --rm mountstatus

### Running the monitor

For testing you can simply run `mount_status_monitor` directly and watch the
output. Note that while the process can run without elevated permissions it is
likely that this will generate error messages due to mountpoints which are
inaccessible.

In normal operation `mount_status_monitor` relies a supervisor such as Upstart,
systemd, or launchd to keep it running. See the `upstart` and `systemd`
directories for provided config files.

## Future Directions

A long term experiment is having `mount_status_monitor` actually attempt to run
`umount -f` (or `umount -f -l` on Linux) and remount the filesystem to reduce
the number of applications which hit a dead mountpoint. This may not be
appropriate for all users and would require testing to avoid making the problem
worse.

## Contributors & Acknowledgements

Thanks to the following Rust community members who volunteered to review &
improve this code, listed in alphabetical order:

- Pascal Hertleif (@killercup)
- Paul Daniel Faria (@Nashenas88)
- Rahul Sharma (@creativcoder)
- Zachary Dremann (@Dr-Emann)

repo_name: acdha/mountstatus

repo_language: Rust


text:
FROM rust:1-buster
ARG PACKAGE_VERSION
RUN if [ -z "${PACKAGE_VERSION}" ]; then echo "--build-arg PACKAGE_VERSION is required"; exit 1; fi

RUN apt-get -qqy update && apt-get -qqy install ruby ruby-dev rubygems build-essential && apt-get clean
RUN gem install --no-ri --no-rdoc --quiet fpm && gem clean

WORKDIR /mountstatus

COPY Cargo.toml Cargo.lock /mountstatus/
COPY src/ /mountstatus/src/
RUN cargo build --release

WORKDIR /package-build

RUN mv /mountstatus/target/release/mount_status_monitor /package-build/
COPY packaging/sysconfig /package-build/
COPY packaging/systemd/mount_status_monitor.service /package-build/

# This is good for a ~60% size reduction:
RUN strip mount_status_monitor

RUN fpm -s dir -t deb -n mount-status-monitor --version ${PACKAGE_VERSION} --replaces MountStatusMonitor --config-files /etc/sysconfig/mount_status_monitor mount_status_monitor=/usr/sbin/mount_status_monitor mount_status_monitor.service=/etc/systemd/system/mount_status_monitor.service sysconfig=/etc/sysconfig/mount_status_monitor

CMD /bin/cp -vr /package-build/*.deb /host-packages-volume


text:
FROM centos:7
ARG PACKAGE_VERSION
RUN if [ -z "${PACKAGE_VERSION}" ]; then echo "--build-arg PACKAGE_VERSION is required"; exit 1; fi

ENV RUST_ARCHIVE=rust-1.71.0-x86_64-unknown-linux-gnu.tar.gz
ENV RUST_DOWNLOAD_URL=https://static.rust-lang.org/dist/$RUST_ARCHIVE

RUN yum --quiet -y update && yum --quiet -y install git gcc curl openssl openssl-devel ca-certificates tar gcc make rpm-build libffi-devel && yum clean all --quiet

RUN yum install --quiet -y centos-release-scl && yum install --quiet -y rh-ruby23 rh-ruby23-rubygems rh-ruby23-ruby-devel && yum clean all --quiet
RUN scl enable rh-ruby23 'gem install --no-ri --no-rdoc json && gem install --no-ri --no-rdoc fpm && gem clean'

WORKDIR /rust

RUN curl -fsOSL $RUST_DOWNLOAD_URL \
    && curl -s $RUST_DOWNLOAD_URL.sha256 | sha256sum -c - \
    && tar -C /rust -xzf $RUST_ARCHIVE --strip-components=1 \
    && rm $RUST_ARCHIVE \
    && ./install.sh --without=rust-docs

WORKDIR /mountstatus

COPY Cargo.toml /mountstatus/
COPY src/ /mountstatus/src/
COPY packaging/systemd/ /mountstatus/systemd/
COPY packaging/sysconfig /mountstatus/

RUN cargo build --release && strip target/release/mount_status_monitor
RUN scl enable rh-ruby23 "fpm -s dir -t rpm --rpm-dist el7 -n mount-status-monitor --version ${PACKAGE_VERSION} --replaces MountStatusMonitor target/release/mount_status_monitor=/usr/sbin/mount_status_monitor systemd/mount_status_monitor.service=/etc/systemd/system/mount_status_monitor.service"

CMD /bin/cp -vr /mountstatus/*.rpm /host-packages-volume


text:
PACKAGE_VERSION = $(shell cargo metadata --format-version=1 | jq -r '.packages[] | select(.name == "mount_status_monitor") | .version')

all: local deb el7

local:
	install -d packages
	cargo clean
	cargo generate-lockfile

deb: local
	podman build -f Dockerfile.release-deb -t mountstatus:release-deb --build-arg PACKAGE_VERSION=${PACKAGE_VERSION} .
	podman run --rm -v $(realpath packages):/host-packages-volume mountstatus:release-deb

el7: local
	podman build -f Dockerfile.release-el7 -t mountstatus:release-el7 --build-arg PACKAGE_VERSION=${PACKAGE_VERSION} .
	podman run --rm -v $(realpath packages):/host-packages-volume mountstatus:release-el7


text:
[package]
name = "mount_status_monitor"
version = "2.1.7"
authors = [ "Chris Adams <chris@improbable.org>" ]
description = "Paranoid filesystem monitor which reports malfunctioning mountpoints"
homepage = "https://github.com/acdha/mountstatus"
repository = "https://github.com/acdha/mountstatus.git"
readme = "README.markdown"
license =  "CC0-1.0"

[dependencies]
wait-timeout = "0.2.0"
libc = "0.2.76"
syslog = "^6.0"
lazy_static = { version = "1.4.0", optional = true }
hostname = { version = "0.3.1", optional = true }
argparse = "0.2.2"
error-chain = "0.12.3"
rayon = "1.3.0"
protobuf = "2.16.2"
log = "0.4.11"

[dependencies.prometheus]
version = "^0.13"
features = ["push"]
optional = true

[features]
default = ["with_prometheus"]
with_prometheus = ["lazy_static", "prometheus", "hostname"]


text:
FROM rust AS builder

WORKDIR /mountstatus

# We'll update the dependencies first so the build / compile stage for all
# of our dependencies can be cached:
# Until https://github.com/rust-lang/cargo/issues/1891 lands we'll use a fake main app
# to avoid clearing our cache except when dependencies actually change.
COPY Cargo.toml Cargo.lock /mountstatus/
RUN mkdir /mountstatus/src && echo "fn main() {}" > /mountstatus/src/main.rs && cargo build

# Now we'll build the actual project:
COPY src/ /mountstatus/src/
RUN cargo build

FROM debian:stretch

# FIXME: we can't test the binary without a local syslog daemon to connect to until we make this configurable:
RUN apt-get -qy update && apt-get -qy install inetutils-syslogd && apt-get clean
RUN echo "*.* /dev/console" > /etc/syslog.conf

COPY --from=builder /mountstatus/target/debug/mount_status_monitor /usr/local/sbin/

ENTRYPOINT service inetutils-syslogd start && timeout 8 mount_status_monitor --poll-interval=5


text:
error_chain! {
    foreign_links {
        Io(::std::io::Error);
    }
}


text:
/*
   Paranoid mount monitor for POSIX operating systems

   The general idea is that some classes of storage failure require care to
   detect because any access to the mountpoint including your monitoring check
   will block and in the case of certain kernel bugs that may be completely
   irrecoverable or will require a considerable delay – often days by default –
   to exhaust repeated TCP and NFS timeouts.

   This can't be solved easily by using asynchronous I/O APIs because key
   platforms like Linux don't implement an async stat(2) equivalent. This
   program uses the broadly-portable approach of launching an external child
   process asynchronously with a timeout. If it fails to respond by the
   deadline, we'll send it a SIGKILL and avoid further checks until the process
   disappears to avoid accumulating blocked check processes.

   The major improvements of the Rust version compared to the original C
   version are the use of persistent state to avoid having more than one check
   pending for any given mountpoint and the ability to send metrics to a
   Prometheus push-gateway so they will be alertable even if the local system
   is severely degraded.
*/

extern crate argparse;
extern crate libc;
extern crate rayon;
extern crate syslog;
extern crate wait_timeout;

#[macro_use]
extern crate log;

#[macro_use]
extern crate error_chain;

#[cfg(feature = "with_prometheus")]
extern crate hostname;

#[cfg(feature = "with_prometheus")]
#[macro_use]
extern crate lazy_static;

#[cfg(feature = "with_prometheus")]
#[macro_use]
extern crate prometheus;

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process;
use std::str;
use std::thread;
use std::time::{Duration, Instant};

use argparse::{ArgumentParser, Print, Store, StoreOption, StoreTrue};
use rayon::prelude::*;
use wait_timeout::ChildExt;

mod errors;
mod get_mounts;

use crate::errors::*;

#[derive(Debug)]
enum MountStatus {
    Alive,
    CheckFailed(i32),
    CheckSignaled(i32),
    CheckRunning {
        process: process::Child,
        start_time: Instant,
    },
}

impl MountStatus {
    fn success(&self) -> bool {
        if let MountStatus::Alive = *self {
            true
        } else {
            false
        }
    }
}

quick_main! { real_main }

fn real_main() -> Result<()> {
    struct Options {
        once_only: bool,
        poll_interval: u64,
        prometheus_push_gateway: Option<String>,
        print_bad_mounts: bool,
    }
    let mut options = Options {
        once_only: false,
        poll_interval: 60,
        prometheus_push_gateway: None,
        print_bad_mounts: false,
    };

    {
        // this block limits scope of borrows by ap.refer() method
        let mut ap = ArgumentParser::new();
        ap.set_description(concat!(
            "Monitor the status of mounted filesystems and report inaccessible mounts.",
            " Dead mounts will be reported to the local syslog server and optionally",
            " a Prometheus push-gateway service."
        ));

        ap.add_option(
            &["--version"],
            Print(env!("CARGO_PKG_VERSION").to_string()),
            "Show version",
        );

        if cfg!(feature = "with_prometheus") {
            ap.refer(&mut options.prometheus_push_gateway).add_option(
                &["--prometheus-push-gateway"],
                StoreOption,
                "Location of the Prometheus push-gateway server to send metrics to",
            );
        }

        ap.refer(&mut options.poll_interval).add_option(
            &["--poll-interval"],
            Store,
            "Number of seconds to wait before checking mounts",
        );

        ap.refer(&mut options.once_only).add_option(
            &["-1", "--once-only"],
            StoreTrue,
            "Check the status once and exit",
        );

        ap.refer(&mut options.print_bad_mounts).add_option(
            &["--print-bad-mounts"],
            StoreTrue,
            "Print bad mounts on standard output",
        );

        ap.parse_args_or_exit();
    }

    let poll_interval_duration = Duration::from_secs(options.poll_interval);

    if !options.once_only {
        println!(
            "mount_status_monitor checking mounts every {} seconds",
            poll_interval_duration.as_secs()
        );
    }

    syslog::init_unix(syslog::Facility::LOG_USER, log::LevelFilter::Debug)
        .chain_err(|| "Unable to connect to syslog")?;

    let mut mount_statuses = HashMap::<PathBuf, MountStatus>::new();

    loop {
        check_mounts(&mut mount_statuses, options.print_bad_mounts);

        // We calculate these values each time because a filesystem may have been
        // mounted or unmounted since the last check:
        let total_mounts = mount_statuses.len();
        let dead_mounts = mount_statuses
            .iter()
            .filter(|&(_, status)| !status.success())
            .count();

        info!("Checked {} mounts; {} are dead", total_mounts, dead_mounts);

        #[cfg(feature = "with_prometheus")]
        {
            if let Some(ref gateway_address) = options.prometheus_push_gateway {
                if let Err(e) = push_to_prometheus(gateway_address, dead_mounts, total_mounts) {
                    eprintln!("{}", e);
                }
            }
        }

        if options.once_only {
            std::process::exit(0);
        }

        // Wait before checking again:
        thread::sleep(poll_interval_duration);
    }
}

#[cfg(feature = "with_prometheus")]
fn push_to_prometheus(
    gateway: &str,
    dead_mounts: usize,
    total_mounts: usize,
) -> prometheus::Result<()> {
    lazy_static! {
        static ref TOTAL_MOUNTS: prometheus::Gauge =
            register_gauge!("total_mountpoints", "Total number of mountpoints").unwrap();
        static ref DEAD_MOUNTS: prometheus::Gauge =
            register_gauge!("dead_mountpoints", "Number of unresponsive mountpoints").unwrap();
    }

    let prometheus_instance = hostname::get().unwrap();

    // The Prometheus metrics are defined as floats so we need to convert;
    // for monitoring the precision loss in general is fine and it's
    // exceedingly unlikely to be relevant when counting the number of
    // mountpoints:
    TOTAL_MOUNTS.set(total_mounts as f64);
    DEAD_MOUNTS.set(dead_mounts as f64);

    prometheus::push_metrics(
        "mount_status_monitor",
        labels! {"instance".to_owned() => String::from(prometheus_instance.to_str().unwrap())},
        gateway,
        prometheus::gather(),
        None,
    )
}

fn check_mounts(mount_statuses: &mut HashMap<PathBuf, MountStatus>, print_bad_mounts: bool) {
    let mount_points = get_mounts::get_mount_points().unwrap_or_else(|err| {
        eprintln!("Failed to retrieve a list of mount-points: {:?}", err);
        std::process::exit(2);
    });

    // Remove any mount status entries which are no longer in the current list of mountpoints:
    mount_statuses.retain(|ref k, _| mount_points.iter().position(|i| *i == **k).is_some());

    for mount_point in mount_points {
        mount_statuses
            .entry(mount_point)
            .or_insert(MountStatus::Alive);
    }

    mount_statuses
        .par_iter_mut()
        .for_each(|(mount_point, mount_status)| {
            if let MountStatus::CheckRunning {
                ref mut process,
                start_time,
            } = *mount_status
            {
                match process.try_wait() {
                    Ok(Some(status)) => {
                        info!(
                            "Slow check for mount {} exited with {} after {} seconds",
                            mount_point.display(),
                            status,
                            start_time.elapsed().as_secs()
                        );
                    }
                    Ok(None) => {
                        warn!(
                            "Slow check for mount {} has not exited after {} seconds",
                            mount_point.display(),
                            start_time.elapsed().as_secs()
                        );
                        return;
                    }
                    Err(e) => {
                        error!(
                            "Stalled check on mount {} returned an error after {} seconds: {}",
                            mount_point.display(),
                            start_time.elapsed().as_secs(),
                            e
                        );
                    }
                }
            }
            let new_mount_status = match check_mount(mount_point) {
                Ok(status) => status,
                Err(e) => {
                    eprintln!("{}", e);
                    return;
                }
            };

            match new_mount_status {
                MountStatus::CheckFailed(rc) => {
                    eprintln!("Mount check failed with an unexpected return code: {}", rc);
                }
                MountStatus::CheckSignaled(signal) => {
                    eprintln!("Mount check was killed by signal: {}", signal);
                }
                _ => {}
            }
            if new_mount_status.success() {
                debug!("Mount passed health-check: {}", mount_point.display());
            } else {
                let msg = format!("Mount failed health-check: {}", mount_point.display());
                eprintln!("{}", msg);
                if print_bad_mounts {
                    println!("{}", mount_point.display())
                }
                error!("{}", msg);
            }

            *mount_status = new_mount_status;
        });
}

fn check_mount(mount_point: &Path) -> Result<MountStatus> {
    let start_time = Instant::now();
    let mut child = process::Command::new("/usr/bin/stat")
        .arg(mount_point)
        .stdout(process::Stdio::null())
        .spawn()
        .chain_err(|| "Unable to spawn process to check mount")?;

    // See https://github.com/rust-lang/rust/issues/18166 for why we can't make this a static value:
    let child_result = child
        .wait_timeout(Duration::from_secs(3))
        .chain_err(|| "Unable to wait on stat command")?;
    match child_result {
        None => {
            /*
                The process has not exited and we're not going to wait for a
                potentially very long period of time for it to recover.

                We'll attempt to clean up the check process by killing it, which
                is defined as sending SIGKILL on Unix:

                https://doc.rust-lang.org/std/process/struct.Child.html#method.kill

                The mount_status structure returned will include this child
                process instance so future checks can perform a non-blocking
                test to see whether it has finally exited:
            */
            if let Err(err) = child.kill() {
                eprintln!("Unable to kill process {}: {:?}", child.id(), err)
            };

            Ok(MountStatus::CheckRunning {
                process: child,
                start_time: start_time,
            })
        }
        Some(exit_status) => {
            let rc = exit_status.code();
            match rc {
                Some(0) => Ok(MountStatus::Alive),
                Some(rc) => Ok(MountStatus::CheckFailed(rc)),
                None => {
                    use std::os::unix::process::ExitStatusExt;

                    // If there isn't a return code, there _should_ always be a signal
                    Ok(MountStatus::CheckSignaled(
                        exit_status.signal().unwrap_or(0),
                    ))
                }
            }
        }
    }
}


text:
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use self::linux::get_mount_points;

#[cfg(all(unix, not(target_os = "linux")))]
mod bsd;
#[cfg(all(unix, not(target_os = "linux")))]
pub use self::bsd::get_mount_points;


text:
// Wrapper for the BSD getmntinfo() API which returns a list of mountpoints

use std::ffi::{CStr, OsStr};
use std::io::{Error, Result};
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use std::ptr;
use std::slice;

use libc::{c_int, statfs};

pub static MNT_NOWAIT: i32 = 2;

extern "C" {
    #[cfg_attr(
        all(target_os = "macos", target_arch = "x86_64"),
        link_name = "getmntinfo$INODE64"
    )]
    #[cfg_attr(
        all(target_os = "macos", target_arch = "aarch64"),
        link_name = "getmntinfo"
    )]
    fn getmntinfo(mntbufp: *mut *mut statfs, flags: c_int) -> c_int;
}

pub fn get_mount_points() -> Result<Vec<PathBuf>> {
    let mut raw_mounts_ptr: *mut statfs = ptr::null_mut();

    let rc = unsafe { getmntinfo(&mut raw_mounts_ptr, MNT_NOWAIT) };

    // getmntinfo() has non-obvious error handling behaviour: rc 0 indicates an
    // error (presumably because any Unix system should have at least the root
    // filesystem), requiring us to check errno for the actual error code. The
    // man pages for FreeBSD and Darwin do not acknowledge the possibility of a
    // negative return code so we'll simply panic if that happens.

    if rc == 0 {
        return Err(Error::last_os_error());
    }

    assert!(rc > 0, "getmntinfo() returned undocumented value: {}", rc);

    assert!(
        !raw_mounts_ptr.is_null(),
        "getmntinfo() returned a null pointer to the list of mountpoints!"
    );

    let raw_mounts = unsafe { slice::from_raw_parts(raw_mounts_ptr, rc as usize) };

    let mounts = raw_mounts
        .iter()
        .map(|m| unsafe {
            let bytes = CStr::from_ptr(&m.f_mntonname[0]).to_bytes();
            PathBuf::from(OsStr::from_bytes(bytes).to_owned())
        })
        .collect();

    Ok(mounts)
}


text:
// Wrapper for the Linux getmntent() API which returns a list of mountpoints

use std::ffi::CStr;
use std::ffi::OsStr;
use std::io::Result;
use std::mem;
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;

use libc::c_char;
use libc::c_int;
use libc::FILE;

#[repr(C)]
#[derive(Debug)]
struct mntent {
    mnt_fsname: *mut c_char,
    mnt_dir: *mut c_char,
    mnt_type: *mut c_char,
    mnt_opts: *mut c_char,
    mnt_freq: c_int,
    mnt_passno: c_int,
}

impl Default for mntent {
    fn default() -> Self {
        unsafe { mem::zeroed() }
    }
}

extern "C" {
    fn getmntent(fp: *mut FILE) -> *mut mntent;
    fn setmntent(filename: *const c_char, _type: *const c_char) -> *mut FILE;
    fn endmntent(fp: *mut FILE) -> c_int;
}

pub fn get_mount_points() -> Result<Vec<PathBuf>> {
    let mut mount_points: Vec<PathBuf> = Vec::new();

    // The Linux API is somewhat baroque: rather than exposing the kernel's view of the world
    // you are expected to provide it with a mounts file which traditionally might have been
    // something like /etc/mtab but should be /proc/self/mounts (n.b. /proc/mounts is just a
    // symlink to /proc/self/mounts).
    let mount_filename = "/proc/self/mounts\0";
    let flags = "r\0";

    let mount_file_handle = unsafe {
        setmntent(
            mount_filename.as_ptr() as *const _,
            flags.as_ptr() as *const _,
        )
    };

    assert!(
        !mount_file_handle.is_null(),
        "Attempting to read mounts from {} failed!",
        &mount_filename[..mount_filename.len() - 1]
    );

    loop {
        let mount_entry = unsafe { getmntent(mount_file_handle) };
        if mount_entry.is_null() {
            break;
        }

        let bytes = unsafe { CStr::from_ptr((*mount_entry).mnt_dir).to_bytes() };
        let mount_point = PathBuf::from(OsStr::from_bytes(bytes).to_owned());
        mount_points.push(mount_point);
    }

    let rc = unsafe { endmntent(mount_file_handle) };
    // The documentation is strong enough about this that there's no plausible
    // way to attempt handling endmntent() failures:
    assert!(
        rc == 1,
        "endmntent() is always supposed to return 1 but returned {}",
        rc
    );

    Ok(mount_points)
}


text:
[Unit]
Description=Paranoid filesystem monitor
After=syslog.target

[Service]
Type=simple
ExecStart=/usr/sbin/mount_status_monitor
Restart=on-failure
RestartSec=10s


text:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>org.improbable.mountstatus</string>
    <key>ProgramArguments</key>
    <array>
      <string>/usr/local/sbin/mount_status_monitor</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
  </dict>
</plist>


text:
description "Paranoid filesystem monitor"
author "Chris Adams <chris@improbable.org>"

start on runlevel [2345]
stop on runlevel [!2345]

console log

respawn
respawn limit unlimited

# Avoid Upstart permanently stopping the job if networking / syslog is down:
post-stop exec sleep 10

script
    . /etc/sysconfig/mount_status_monitor
    exec /usr/sbin/mount_status_monitor ${EXTRA_ARGS:-}
end script


text:
package main

import (
	"github.com/dan-v/dosxvpn/cmd"
)

func main() {
	cmd.Execute()
}

repo_name: dan-v/dosxvpn

repo_language: Go

mime_type: text/x-shellscript


text:
all: osx

osx:
	GOOS=darwin GOARCH=amd64 go build -o ./build/osx/x86-64/dosxvpn .
	cd build/osx/x86-64 && zip -r ./dosxvpn-cli.zip ./dosxvpn
	cd platypus && ./build.sh
	cd build/osx/x86-64 && zip -r ./dosxvpn-app.zip ./dosxvpn.app

clean:
	rm -rf build

.PHONY: osx