diff --git a/api/graphql.js b/api/graphql.js index 005b997..3c130df 100644 --- a/api/graphql.js +++ b/api/graphql.js @@ -28,25 +28,26 @@ class ResumeSchema { const ResumeType = new GraphQLObjectType({ name: 'resume', - fields: function () { - return { - id: { - type: GraphQLID - }, - userid: { - type: GraphQLFloat - }, - resumeid: { - type: GraphQLInt - }, - cvdata: { - type: GraphQLString - }, - template: { - type: GraphQLString - } - }; - } + fields: () => ({ + id: { + type: new GraphQLNonNull(GraphQLID) + }, + userid: { + type: new GraphQLNonNull(GraphQLFloat) + }, + resumeid: { + type: new GraphQLNonNull(GraphQLInt) + }, + cvdata: { + type: GraphQLString + }, + template: { + type: GraphQLString + }, + share: { + type: GraphQLString + } + }) }); const MutationAdd = { @@ -72,13 +73,17 @@ class ResumeSchema { template: { name: 'Resume template', type: new GraphQLNonNull(GraphQLString) + }, + share: { + name: 'Resume share', + type: new GraphQLNonNull(GraphQLString) } }, - resolve: (root, {id, userid, resumeid, cvdata, template}) => { + resolve: (root, {id, userid, resumeid, cvdata, template, share}) => { return new Promise((resolve, reject) => { - db.insert(dbName, {id, userid, resumeid, cvdata, template}) + db.insert(dbName, {id, userid, resumeid, cvdata, template, share}) .then(() => db.selectAll(dbName)) - .then(resumes => resolve(resumes)) + .then(() => resolve()) .catch(err => reject(err)); }); } @@ -97,7 +102,7 @@ class ResumeSchema { return new Promise((resolve, reject) => { db.delete(dbName, {id}) .then(() => db.selectAll(dbName)) - .then(resumes => resolve(resumes)) + .then(() => resolve()) .catch(err => reject(err)); }); } @@ -108,7 +113,7 @@ class ResumeSchema { description: 'Update a Resume', args: { id: { - name: 'Id', + name: 'Resume Id', type: new GraphQLNonNull(GraphQLID) }, cvdata: { @@ -118,15 +123,20 @@ class ResumeSchema { template: { name: 'Resume template', type: GraphQLString + }, + share: { + name: 'Resume share', + type: GraphQLString } }, - resolve: (root, {id, cvdata, template}) => { + resolve: (root, {id, cvdata, template, share}) => { return new Promise((resolve, reject) => { const dataToUpdate = {}; if(cvdata) dataToUpdate.cvdata = cvdata; if(template) dataToUpdate.template = template; + if(share) dataToUpdate.share = share; db.update(dbName, {id}, {$set:dataToUpdate}) - .then(() => db.selectAll(dbName)) + .then(() => db.find(dbName, {id})) .then(resumes => resolve(resumes)) .catch(err => reject(err)); }); @@ -143,12 +153,17 @@ class ResumeSchema { args: { userid: { name: 'User ID', - type: new GraphQLNonNull(GraphQLFloat) + type: GraphQLFloat + }, + id: { + name: 'Resume ID', + type: GraphQLID } }, - resolve: (root, {userid}) => { + resolve: (root, {id, userid}) => { return new Promise((resolve, reject) => { - db.find(dbName, {userid}) + const idToFind = id ? {id} : {userid}; + db.find(dbName, idToFind) .then(resumes => resolve(resumes)) .catch(err => reject(err)); }); diff --git a/package.json b/package.json index 80fdf8b..7f69c37 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "draft-js-import-html": "0.3.2", "es6-promise": "4.1.1", "express": "4.14.1", - "express-graphql": "0.6.6", + "express-graphql": "0.6.12", "express-session": "1.15.3", "extract-text-webpack-plugin": "2.1.2", "file-loader": "0.9.0", diff --git a/tools/middlewares/cryptoModule.js b/tools/middlewares/cryptoModule.js new file mode 100644 index 0000000..0a7d50b --- /dev/null +++ b/tools/middlewares/cryptoModule.js @@ -0,0 +1,20 @@ +import crypto from 'crypto'; + +const key = process.env.cryptoKey; +const nonce = process.env.cryptoNonce; + +const encrypt = (text) => { + const cipher = crypto.createCipheriv('aes256', key, nonce); + let ciphertext = cipher.update(text, 'utf8', 'hex'); + ciphertext += cipher.final('hex'); + return ciphertext; +}; + +const decrypt = (ciphertext) => { + const decipher = crypto.createDecipheriv('aes256', key, nonce); + let plainText = decipher.update(ciphertext, 'hex', 'utf8'); + plainText += decipher.final('utf8'); + return plainText; +}; + +export {encrypt, decrypt}; diff --git a/tools/middlewares/graphql-query.js b/tools/middlewares/graphql-query.js new file mode 100644 index 0000000..c7f7ac5 --- /dev/null +++ b/tools/middlewares/graphql-query.js @@ -0,0 +1,24 @@ +import { graphql } from 'graphql'; + +import ResumeSchema from '../../api/graphql'; + +const graphqlQuery = (query) => { + + return new Promise((resolve, reject) => { + graphql(getSchema(), query).then((result) => { + if (result.errors) reject(result.errors); + if (result.data.resumes && result.data.resumes.length === 1) + resolve(result.data.resumes[0]); + if (result.data.resumes && result.data.resumes.length > 1) + resolve(result.data.resumes); + resolve(); + }).catch(e => reject(e)); + }); +}; + +const getSchema = () => { + const resumeSchema = new ResumeSchema().getSchema(); + return resumeSchema; +}; + +export {getSchema, graphqlQuery}; diff --git a/tools/routes/api.js b/tools/routes/api.js index dc49d7b..caa57ac 100644 --- a/tools/routes/api.js +++ b/tools/routes/api.js @@ -1,23 +1,25 @@ -import { graphql } from 'graphql'; import graphqlHTTP from 'express-graphql'; import bodyParser from 'body-parser'; -import ResumeSchema from '../../api/graphql'; +import {getSchema, graphqlQuery} from '../middlewares/graphql-query'; const ApiRouter = (app, express) => { const router = express.Router(); - const resumeSchema = new ResumeSchema().getSchema(); + const resumeSchema = getSchema(); - router.use('/graphql', graphqlHTTP(() => ({ - resumeSchema, - pretty: true - }))); + router.use('/graphql', graphqlHTTP({ + schema: resumeSchema, + pretty: true, + graphiql: true + })); router.post('/resume', bodyParser.text(), (req, res) => { - graphql(resumeSchema, req.body).then( function(result) { - res.send(JSON.stringify(result,null,' ')); - }).catch(e => res.send(e)); + graphqlQuery(req.body).then( function(result) { + res.send({result}); + }).catch(e => { + res.status(502).send({errors: e}); + }); }); return router; diff --git a/tools/routes/app.js b/tools/routes/app.js index 4f523f4..23d05a1 100644 --- a/tools/routes/app.js +++ b/tools/routes/app.js @@ -3,6 +3,8 @@ import path from 'path'; import Mailer from '../middlewares/mailer_sendgrid'; import Messager from '../middlewares/messager'; import {generatePDF} from '../middlewares/generate-pdf'; +import {encrypt, decrypt} from '../middlewares/cryptoModule'; +import {graphqlQuery} from '../middlewares/graphql-query'; import * as Templates from '../../web/templates'; const AppRoutes = (app, express) => { @@ -11,7 +13,7 @@ const AppRoutes = (app, express) => { const mailService = new Mailer(); const messager = new Messager(); - router.post('/download', bodyParser.json() , function(req, res){ + router.post('/download', bodyParser.json() , (req, res) => { let Comp = Templates[`Template${req.body.templateId}`]; const filename = `Resume-${req.body.templateId}-${new Date().getTime()}`; const filePath = path.join(__dirname,`../generated_files/${filename}.pdf`); @@ -19,11 +21,11 @@ const AppRoutes = (app, express) => { .then((response)=> res.send(response)) .catch((error) => { console.error('Error downloading resume', error); - res.status(500).send('Error downloading resume. Please try again after some time'); + res.status(502).send({errors: 'Error downloading resume. Please try again after some time'}); }); }); - router.post('/email', bodyParser.json() , function(req, res){ + router.post('/email', bodyParser.json() , (req, res) => { let Comp = Templates[`Template${req.body.templateId}`]; const filename = `Resume-${req.body.templateId}-${new Date().getTime()}`; const filePath = path.join(__dirname,`../generated_files/${filename}.pdf`); @@ -32,18 +34,44 @@ const AppRoutes = (app, express) => { .then(() => res.send({ok: true, statusText: 'Email sent successfully'})) .catch((error) => { console.error('Error emailing resume', error); - res.status(500).send({ok: false, statusText: 'Error sending resume. Please try again after sometime'}); + res.status(502).send({errors: 'Error sending resume. Please try again after sometime'}); }); }); - router.get('/template/:id', bodyParser.json() , function(req, res){ + router.post('/share', bodyParser.json() , (req, res) => { + const ciphertext = encrypt(req.body.id); + const link = `${req.protocol}://${req.headers.host}/resume/${ciphertext}`; + const share = JSON.stringify(JSON.stringify({link})); + var query = `mutation { update (id: "${req.body.id}", share: ${share}) { id } }`; + graphqlQuery(query).then(() => { + res.send({result: {link}}); + }).catch((error) => { + console.error('Error sharing resume', error); + res.status(502).send({errors: 'Error sharing resume. Please try again after sometime'}); + }); + }); + + router.get('/resume/:id', (req, res) => { + const decryptedId = decrypt(req.params.id); + var query = `query { resumes (id: "${decryptedId}") { id, resumeid, cvdata, template } }`; + graphqlQuery(query).then((result) => { + let Comp = Templates[`Template${JSON.parse(result.template).id}`]; + const html = app.locals.getComponentAsHTML(Comp, JSON.parse(result.cvdata), JSON.parse(result.template).color); + res.send(html); + }).catch((error) => { + console.error('Error getting resume', error); + res.status(502).send({errors: 'Error getting resume. Please try again after sometime'}); + }); + }); + + router.get('/template/:id', bodyParser.json() , (req, res) => { const json = require('../../mock/snehajain.json'); let Comp = Templates[`Template${req.params.id}`]; const html = app.locals.getComponentAsHTML(Comp, json); res.send(html); }); - router.post('/feedback', bodyParser.json() , function(req, res){ + router.post('/feedback', bodyParser.json() , (req, res) => { messager.sendFeedback(req.body); mailService.sendFeedback(req.body); res.sendStatus(204); diff --git a/tools/server.js b/tools/server.js index bcf7e7b..396dbf6 100644 --- a/tools/server.js +++ b/tools/server.js @@ -46,9 +46,9 @@ app.locals.renderIndex = (res, data) => { }, res); }; -app.locals.getComponentAsHTML = (Component, cvdata, designColor) => { +app.locals.getComponentAsHTML = (Component, cvdata, templateColor) => { try { - return ReactDOMServer.renderToStaticMarkup(); + return ReactDOMServer.renderToStaticMarkup(); } catch (e) { console.error(e); return '
Some Error Occured
'; diff --git a/web/actions/index.js b/web/actions/index.js index 0600d6c..f7b5e5e 100644 --- a/web/actions/index.js +++ b/web/actions/index.js @@ -2,3 +2,4 @@ export * from './analytics'; export * from './cvform/'; export * from './app'; export * from './template'; +export * from './share'; diff --git a/web/actions/share.js b/web/actions/share.js new file mode 100644 index 0000000..f2b86a3 --- /dev/null +++ b/web/actions/share.js @@ -0,0 +1,6 @@ +const changeShareLink = (link) => ({ + type: 'CHANGE_SHARE_LINK', + payload: link +}); + +export {changeShareLink}; diff --git a/web/api/resume.js b/web/api/resume.js index b6321b7..cadfaaa 100644 --- a/web/api/resume.js +++ b/web/api/resume.js @@ -7,77 +7,80 @@ promise.polyfill(); class ResumeService { - static add(user, resumeid, cvdata, templateid, templatecolor) { + handleResponse(res) { + return new Promise((resolve, reject) => { + if (res.ok || res.status === 502) { + res.json().then(data => { + if (data.errors) reject(data.errors); + else resolve(data.result); + }); + } else reject(res.statusText); + }); + } + + add(user, resumeid, cvdata, templateid, templatecolor, share) { const template = JSON.stringify(JSON.stringify({id: templateid, color: templatecolor})); - var query = `mutation { add (id: "${user.id}_${resumeid}", userid: ${user.id}, resumeid: ${resumeid}, cvdata: ${JSON.stringify(JSON.stringify(cvdata))}, template: ${template}) { id } }`; - return new Promise((resolve) => { + var query = `mutation { add (id: "${user.id}_${resumeid}", userid: ${user.id}, resumeid: ${resumeid}, cvdata: ${JSON.stringify(JSON.stringify(cvdata))}, template: ${template}, share: ${JSON.stringify(JSON.stringify(share))}) { id } }`; + return new Promise((resolve, reject) => { fetch('/api/resume', { method: 'POST', body: query - }).then(data => data.json()) - .then((data) => { - if (data.errors) throw data.errors; - else resolve({}); - }).catch((err) => { - window.sendErr('ResumeService add err', err); - resolve({}); + }).then(this.handleResponse) + .then(() => resolve()) + .catch((err) => { + window.sendErr(`ResumeService add err: ${JSON.stringify(err)}`); + reject(); }); }); } - static get(user) { - var query = `query { resumes (userid: ${user.id}) { id, resumeid, cvdata, template } }`; - return new Promise((resolve) => { + get(user) { + var query = `query { resumes (userid: ${user.id}) { id, resumeid, cvdata, template, share } }`; + return new Promise((resolve, reject) => { fetch('/api/resume', { method: 'POST', body: query - }).then(data => data.json()) - .then((data) => { - if (data.errors) throw data.errors; - else resolve(data); - }).catch((err) => { - window.sendErr('ResumeService get err:', err); - resolve({data: {resumes: []}}); + }).then(this.handleResponse) + .then((data) => resolve(data)) + .catch((err) => { + window.sendErr(`ResumeService get err: ${JSON.stringify(err)}`); + reject(); }); }); } - static update(user, resumeid, cvdata) { - return new Promise((resolve) => { + update(user, resumeid, cvdata) { + return new Promise((resolve, reject) => { var query = `mutation { update (id: "${user.id}_${resumeid}", cvdata: ${JSON.stringify(JSON.stringify(cvdata))}) { id } }`; fetch('/api/resume', { method: 'POST', body: query - }).then(data => data.json()) - .then((res) => { - if (res.errors) throw res.errors; - else resolve(); - }).catch((err) => { - window.sendErr('ResumeService update err:', err); - resolve(); + }).then(this.handleResponse) + .then(() => resolve()) + .catch((err) => { + window.sendErr(`ResumeService update err: ${JSON.stringify(err)}`); + reject(); }); }); } - static updateTemplate(user, resumeid, templateid, templatecolor) { + updateTemplate(user, resumeid, templateid, templatecolor) { const template = JSON.stringify(JSON.stringify({id: templateid, color: templatecolor})); - return new Promise((resolve) => { + return new Promise((resolve, reject) => { var query = `mutation { update (id: "${user.id}_${resumeid}", template: ${template}) { id } }`; fetch('/api/resume', { method: 'POST', body: query - }).then(data => data.json()) - .then((res) => { - if (res.errors) throw res.errors; - else resolve(); - }).catch((err) => { - window.sendErr('ResumeService update template err:', err); - resolve(); + }).then(this.handleResponse) + .then(() => resolve()) + .catch((err) => { + window.sendErr(`ResumeService updateTemplate err: ${JSON.stringify(err)}`); + reject(); }); }); } - static download(data) { + download(data) { return new Promise((resolve, reject) => { fetch('/download', { method: 'POST', @@ -94,13 +97,13 @@ class ResumeService { fileSaver.saveAs(blob, 'resume.pdf'); resolve(); }).catch((err) => { - window.sendErr('ResumeService download err:', err); + window.sendErr(`ResumeService download err: ${JSON.stringify(err)}`); reject(err); }); }); } - static email(data) { + email(data) { return new Promise((resolve, reject) => { fetch('/email', { method: 'POST', @@ -108,14 +111,29 @@ class ResumeService { 'Content-Type': 'application/json' }, body: data - }).then((res) => { - if (res.ok) - resolve(); - else throw res.statusText; - }).catch((err) => { - window.sendErr('ResumeService email err:', err); - reject({message: err}); - }); + }).then(this.handleResponse) + .then(() => resolve()) + .catch((err) => { + window.sendErr(`ResumeService email err: ${JSON.stringify(err)}`); + reject(); + }); + }); + } + + share(data) { + return new Promise((resolve, reject) => { + fetch('/share', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: data + }).then(this.handleResponse) + .then(({link}) => resolve(link)) + .catch((err) => { + window.sendErr(`ResumeService share err: ${JSON.stringify(err)}`); + reject(); + }); }); } } diff --git a/web/clientRenderer.js b/web/clientRenderer.js index ee7b006..8bf033c 100644 --- a/web/clientRenderer.js +++ b/web/clientRenderer.js @@ -77,38 +77,29 @@ if (document) { }; const main = () => { - try { - /* eslint-disable no-underscore-dangle */ - const preloadedState = window.__REDUX_STATE__; - if (!preloadedState.user) { - const user_id = UserService.get(); - if (user_id) { - preloadedState.user = {id: user_id}; - } else { - preloadedState.user = {id: new Date().getTime()}; - UserService.add(preloadedState.user); - } - preloadedState.user.isLoggedIn = false; + /* eslint-disable no-underscore-dangle */ + const preloadedState = window.__REDUX_STATE__; + const resumeService = new ResumeService(); + if (!preloadedState.user) { + const user_id = UserService.get(); + if (user_id) { + preloadedState.user = {id: user_id}; + } else { + preloadedState.user = {id: new Date().getTime()}; + UserService.add(preloadedState.user); } - ResumeService.get(preloadedState.user).then(res => { - if (res.errors) { - throw `ResumeService get error: ${res.errors}`; - } - if (res.data.resumes.length === 0) { - preloadedState.user.isNew = true; - } else { - preloadedState.cvform = htmlToJson(res.data.resumes[0].cvdata); - preloadedState.template = JSON.parse(res.data.resumes[0].template); - } - render(preloadedState); - }).catch((e) => { - window.sendErr(e.message); - showError(); - }); - } catch (err) { - window.sendErr(err.stack); - showError(); + preloadedState.user.isLoggedIn = false; } + resumeService.get(preloadedState.user).then(res => { + if (!res) { + preloadedState.user.isNew = true; + } else { + preloadedState.cvform = htmlToJson(res.cvdata); + preloadedState.template = JSON.parse(res.template); + preloadedState.share = JSON.parse(res.share); + } + render(preloadedState); + }).catch(() => showError()); }; main(); } diff --git a/web/components/email-dialog/index.js b/web/components/email-dialog/index.js index 68be9df..5be68ef 100644 --- a/web/components/email-dialog/index.js +++ b/web/components/email-dialog/index.js @@ -25,6 +25,7 @@ class EmailDialog extends React.Component{ emailError: null, emailSucess: false }; + this.resumeService = new ResumeService(); this.email = this.email.bind(this); this.handleClose = this.handleClose.bind(this); this.handleFormChange = this.handleFormChange.bind(this); @@ -38,7 +39,7 @@ class EmailDialog extends React.Component{ if (this.state.fullname.error || this.state.email.error || this.state.message.error) { return; } - this.props.trackEmail(); + this.props.fireButtonClick('email'); this.setState({emailing: true}); const data = JSON.stringify({ cvdata: this.props.cvdata, @@ -50,8 +51,8 @@ class EmailDialog extends React.Component{ message: this.state.message.value } }); - ResumeService.updateTemplate(this.props.user, 1, this.props.templateId, this.props.templateColor) - .then(() => ResumeService.email(data)) + this.resumeService.updateTemplate(this.props.user, 1, this.props.templateId, this.props.templateColor) + .then(() => this.resumeService.email(data)) .then(() => { this.setState({ fullname: {value: '', error: ''}, @@ -171,11 +172,11 @@ const mapStateToProps = (state) => ({ }); const mapDispatchToProps = dispatch => ({ - trackEmail: () => dispatch(ACTIONS.fireButtonClick('email')) + fireButtonClick: (buttonName) => dispatch(ACTIONS.fireButtonClick(buttonName)) }); EmailDialog.propTypes = { - trackEmail: PropTypes.func.isRequired, + fireButtonClick: PropTypes.func.isRequired, user: PropTypes.object.isRequired, cvdata: PropTypes.object.isRequired, templateId: PropTypes.number.isRequired, diff --git a/web/components/form-feedback/index.js b/web/components/form-feedback/index.js index 5428cc4..8069afc 100644 --- a/web/components/form-feedback/index.js +++ b/web/components/form-feedback/index.js @@ -24,6 +24,10 @@ class FormFeedback extends React.Component{ } submitForm(event) { event.preventDefault(); + ['fullname', 'email', 'message'].forEach((property) => { + if (!this.state[property].value) + this.handleChange({target: {value: '', required: true}}, property); + }); if (this.state.fullname.error || this.state.email.error || this.state.message.error) return; var postData = { fullname: this.state.fullname.value, @@ -95,7 +99,7 @@ class FormFeedback extends React.Component{ label="Submit" primary={true} onClick={this.submitForm} - style={{marginTop: '12px'}} + style={{marginTop: '28px'}} /> ); diff --git a/web/components/page-header/index.js b/web/components/page-header/index.js index dad3149..894fe94 100644 --- a/web/components/page-header/index.js +++ b/web/components/page-header/index.js @@ -33,7 +33,8 @@ class PageHeader extends React.Component { window.addEventListener('resize', this.handleWidth); /* global gapi FB*/ window.onGapiLoaded = () => { - gapi.follow.go(); + gapi.follow.go('g-follow'); + gapi.plusone.render('g-plusone'); this.enableLike(); }; window.onFbLoaded = () => { @@ -48,7 +49,7 @@ class PageHeader extends React.Component { handleWidth() { this.props.changeView({ - mobileView: window.innerWidth <= 604 + mobileView: window.innerWidth <= 768 }); } @@ -112,14 +113,14 @@ class PageHeader extends React.Component { Contact - {this.state.displayLike && +

Like Us:

  • -
  • -
  • +
  • +
-
} +
} diff --git a/web/components/share-dialog/index.js b/web/components/share-dialog/index.js new file mode 100644 index 0000000..80b45ce --- /dev/null +++ b/web/components/share-dialog/index.js @@ -0,0 +1,105 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import muiThemeable from 'material-ui/styles/muiThemeable'; +import Dialog from 'material-ui/Dialog'; +import RaisedButton from 'material-ui/RaisedButton'; + +import {ResumeService} from '../../api'; +import {jsonToHtml} from '../../utils/parse-cvform'; +import * as ACTIONS from '../../actions'; + +import './small.less'; + +class ShareDialog extends React.Component{ + + constructor(props) { + super(props); + this.state = { + sharing: false, + shareError: null, + emailSucess: false + }; + this.share = this.share.bind(this); + this.resumeService = new ResumeService(); + } + + componentWillReceiveProps(nextProps) { + if (!this.props.isOpen && nextProps.isOpen && !this.props.share.link) { + this.share(); + } + } + + share() { + this.props.fireButtonClick('share'); + this.setState({sharing: true}); + this.resumeService.updateTemplate(this.props.user, 1, this.props.templateId, this.props.templateColor) + .then(() => this.resumeService.share(JSON.stringify({id: `${this.props.user.id}_1`}))) + .then((link) => { + this.setState({ + sharing: false + }); + this.props.updateLink(link); + }).catch(() => { + this.setState({sharing: false, shareError: 'An error occured. please try again'}); + }); + } + + render() { + return ( +
+ ]} + modal={false} + open={this.props.isOpen} + onRequestClose={this.props.toggle} > +
+

{this.state.shareError}

+ {this.state.sharing ? 'Generating link...' : {this.props.share.link}} +
+
+
+ ); + } +} + +const mapStateToProps = (state) => ({ + cvdata: jsonToHtml(state.cvform), + user: state.user, + templateId: state.template.id, + templateColor: state.template.color, + mobileView: state.app.mobileView, + share: state.share +}); + +const mapDispatchToProps = dispatch => ({ + fireButtonClick: (buttonName) => dispatch(ACTIONS.fireButtonClick(buttonName)), + updateLink: (link) => dispatch(ACTIONS.changeShareLink(link)) +}); + +ShareDialog.propTypes = { + fireButtonClick: PropTypes.func.isRequired, + user: PropTypes.object.isRequired, + cvdata: PropTypes.object.isRequired, + templateId: PropTypes.number.isRequired, + templateColor: PropTypes.string.isRequired, + toggle: PropTypes.func.isRequired, + isOpen: PropTypes.bool.isRequired, + share: PropTypes.object.isRequired, + updateLink: PropTypes.func.isRequired +}; + +export default muiThemeable()(connect( + mapStateToProps, + mapDispatchToProps +)(ShareDialog)); diff --git a/web/components/share-dialog/small.less b/web/components/share-dialog/small.less new file mode 100644 index 0000000..0cd04af --- /dev/null +++ b/web/components/share-dialog/small.less @@ -0,0 +1,16 @@ +@import '../../common/styles/colors.less'; + +.share-dialog { + .email-form { + max-width: 435px; + margin: auto; + } + .error { + color: @errorColor; + font-size: 14px; + } + a { + color: @primary1Color; + text-decoration: underline; + } +} diff --git a/web/index.js b/web/index.js index de643bc..7012bdc 100644 --- a/web/index.js +++ b/web/index.js @@ -13,11 +13,12 @@ class Page extends React.Component { constructor(props) { super(props); + this.resumeService = new ResumeService(); this.reduxStore = configureStore(props.preloadedState); this.reduxStore.dispatch(ACTIONS.initState()); const state = this.reduxStore.getState(); if (state.user.isNew) - ResumeService.add(state.user, 1, jsonToHtml(state.cvform), state.template.id, state.template.color); + this.resumeService.add(state.user, 1, jsonToHtml(state.cvform), state.template.id, state.template.color, state.share).catch(e => console.log(e)); } componentDidMount() { diff --git a/web/pages/editor/index.js b/web/pages/editor/index.js index 5e9cc9c..62a8fc1 100644 --- a/web/pages/editor/index.js +++ b/web/pages/editor/index.js @@ -30,6 +30,7 @@ class Editor extends React.Component { this.state = { stepIndex: 0 }; + this.resumeService = new ResumeService(); this.stepCount = 6; this.steps = ['Personal', 'Profile', 'Skill', 'Job', 'Education', 'Misc']; this.preview = this.preview.bind(this); @@ -45,7 +46,7 @@ class Editor extends React.Component { preview() { this.props.trackPreview(); - ResumeService.update(this.props.user, 1, jsonToHtml(this.props.cvdata)) + this.resumeService.update(this.props.user, 1, jsonToHtml(this.props.cvdata)) .then(() => browserHistory.push('/preview')) .catch(() => alert('Some error occured. Please try again')); } diff --git a/web/pages/editor/small.less b/web/pages/editor/small.less index d8e5792..12f30ea 100644 --- a/web/pages/editor/small.less +++ b/web/pages/editor/small.less @@ -32,14 +32,14 @@ } } .form-section { - max-width: 604px; + max-width: 768px; padding: 24px; margin: auto; padding-bottom: 24px; } } -@media all and (max-width: 604px) { +@media all and (max-width: 768px) { .editor { .tabs { > div:first-child { diff --git a/web/pages/preview/index.js b/web/pages/preview/index.js index 401f6ab..507172c 100644 --- a/web/pages/preview/index.js +++ b/web/pages/preview/index.js @@ -6,10 +6,14 @@ import { browserHistory } from 'react-router'; import muiThemeable from 'material-ui/styles/muiThemeable'; import RaisedButton from 'material-ui/RaisedButton'; import {Toolbar} from 'material-ui/Toolbar'; +import IconMenu from 'material-ui/IconMenu'; +import MenuItem from 'material-ui/MenuItem'; import DownloadIcon from 'material-ui/svg-icons/file/cloud-download'; import EmailIcon from 'material-ui/svg-icons/communication/email'; import ChevronLeft from 'material-ui/svg-icons/navigation/chevron-left'; import PrintIcon from 'material-ui/svg-icons/action/print'; +import ShareIcon from 'material-ui/svg-icons/social/share'; +import LinkIcon from 'material-ui/svg-icons/content/link'; import ColorLens from 'material-ui/svg-icons/image/color-lens'; import {ResumeService} from '../../api'; @@ -19,6 +23,7 @@ import * as ACTIONS from '../../actions'; import * as Templates from '../../templates'; import PageHeaderContainer from '../../containers/page-header'; import EmailDialog from '../../components/email-dialog'; +import ShareDialog from '../../components/share-dialog'; import './small.less'; @@ -28,11 +33,14 @@ class Preview extends React.Component { this.state = { error: null, downloading: false, - emailDialogOpen: false + emailDialogOpen: false, + shareDialogOpen: false }; this.download = this.download.bind(this); this.print = this.print.bind(this); this.toggleEmailDialog = this.toggleEmailDialog.bind(this); + this.toggleShareDialog = this.toggleShareDialog.bind(this); + this.resumeService = new ResumeService(); } choose() { @@ -43,16 +51,20 @@ class Preview extends React.Component { this.setState({emailDialogOpen: !this.state.emailDialogOpen}); } + toggleShareDialog() { + this.setState({shareDialogOpen: !this.state.shareDialogOpen}); + } + download() { - this.props.trackDownload(); + this.props.fireButtonClick('download'); this.setState({downloading: true}); const data = JSON.stringify({ cvdata: this.props.cvdata, templateId: this.props.templateId, templateColor: this.props.templateColor }); - ResumeService.updateTemplate(this.props.user, 1, this.props.templateId, this.props.templateColor) - .then(ResumeService.download(data)) + this.resumeService.updateTemplate(this.props.user, 1, this.props.templateId, this.props.templateColor) + .then(this.resumeService.download(data)) .then(() => this.setState({downloading: false})) .catch(e => this.setState({downloading: false, error: e.message})); } @@ -62,30 +74,42 @@ class Preview extends React.Component { } print() { - this.props.trackPrint(); - ResumeService.updateTemplate(this.props.user, 1, this.props.templateId, this.props.templateColor) + this.props.fireButtonClick('print'); + this.resumeService.updateTemplate(this.props.user, 1, this.props.templateId, this.props.templateColor) .then(() => window.print()) .catch(e => this.setState({error: e.message})); } render() { let Comp = Templates[`Template${this.props.templateId}`] || Templates['Template1']; + const style = { + minWidth: '48px', + marginLeft: '8px' + }; + const labelStyle = {paddingRight: 8}; + const buttonStyle = {}; + const allStyles = {style, labelStyle, buttonStyle}; return (
- }/> - } - /> - } /> + }/> + }/> + } />
- } /> - } /> - } /> + } />} + open={this.state.openMenu} + onRequestChange={this.handleOnRequestChange} + > + } /> + }/> + }/> + + } />
@@ -94,6 +118,7 @@ class Preview extends React.Component {
+ ); } @@ -111,8 +136,7 @@ const mapDispatchToProps = dispatch => ({ changeTemplateColor: (e) => { dispatch(ACTIONS.changeTemplateColor(e.target.value)); }, - trackDownload: () => dispatch(ACTIONS.fireButtonClick('download')), - trackPrint: () => dispatch(ACTIONS.fireButtonClick('print')) + fireButtonClick: (buttonName) => dispatch(ACTIONS.fireButtonClick(buttonName)) }); export default muiThemeable()(connect( @@ -127,8 +151,7 @@ Preview.propTypes = { templateId: PropTypes.number.isRequired, templateColor: PropTypes.string.isRequired, changeTemplateColor: PropTypes.func.isRequired, - trackDownload: PropTypes.func.isRequired, - trackPrint: PropTypes.func.isRequired + fireButtonClick: PropTypes.func.isRequired }; Preview.defaultProps = {}; diff --git a/web/pages/preview/small.less b/web/pages/preview/small.less index 247e1cf..ac9e943 100644 --- a/web/pages/preview/small.less +++ b/web/pages/preview/small.less @@ -14,12 +14,10 @@ justify-content: space-between; } .colorpicker { - margin-top: 0 !important; - margin-right: 12px; - height: 40px !important; + height: 37px!important; width: 48px; padding: 0; - margin-top: -2px !important; + margin-top: 0 !important; } } .error { diff --git a/web/pages/template/index.js b/web/pages/template/index.js index 3fe2083..da90790 100644 --- a/web/pages/template/index.js +++ b/web/pages/template/index.js @@ -6,7 +6,6 @@ import { connect } from 'react-redux'; import muiThemeable from 'material-ui/styles/muiThemeable'; import {Toolbar} from 'material-ui/Toolbar'; import RaisedButton from 'material-ui/RaisedButton'; -import Avatar from 'material-ui/Avatar'; import PreviewIcon from 'material-ui/svg-icons/action/visibility'; import PageHeaderContainer from '../../containers/page-header'; @@ -22,13 +21,14 @@ class Template extends React.Component { constructor(props) { super(props); this.preview = this.preview.bind(this); + this.resumeService = new ResumeService(); } preview() { this.props.trackPreview(); const templateid = this.props.templateId; const templatecolor = tilesData[this.props.templateId - 1].templateColor; - ResumeService.updateTemplate(this.props.user, 1, templateid, templatecolor) + this.resumeService.updateTemplate(this.props.user, 1, templateid, templatecolor) .then(() => browserHistory.push('/preview')) .catch(() => alert('Some error occured. Please try again')); } diff --git a/web/reducers/index.js b/web/reducers/index.js index 90d0602..cd23e4c 100644 --- a/web/reducers/index.js +++ b/web/reducers/index.js @@ -6,12 +6,14 @@ import build from './build'; import template from './template'; import user from './user'; import app from './app'; +import share from './share'; const reducer = combineReducers({ cvform, user, build, template, + share, app, routing: routerReducer }); diff --git a/web/reducers/share.js b/web/reducers/share.js new file mode 100644 index 0000000..12f2c72 --- /dev/null +++ b/web/reducers/share.js @@ -0,0 +1,17 @@ +const initialState = { + link: null +}; + +const share = (state = initialState, action) => { + + switch (action.type) { + + case 'CHANGE_SHARE_LINK': + return {...state, link: action.payload}; + + default: + return { ...state }; + } +}; + +export default share;