Skip to content

Commit 9eaf534

Browse files
Malek Hakimlorensr
authored andcommitted
Enable email form, fixes #2
* Supports Subscriber model on Apollo Server * Better Error Handling on Apollo Server * Adds FormSubscribe Component * Minor fixes * Fix merge * Fixes after code review * Fix formatting * Support for onSubmit
1 parent 8f512c6 commit 9eaf534

File tree

15 files changed

+1663
-305
lines changed

15 files changed

+1663
-305
lines changed

.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"extends": "airbnb",
77
"parser": "babel-eslint",
88
"rules": {
9-
"semi": 0,
9+
"semi": ["error", "never"],
1010
"no-unused-vars": 0,
1111
"arrow-body-style": 0,
1212
"react/jsx-filename-extension": 0,

components/SubscribeForm.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React, { Component, PropTypes } from 'react'
2+
import { graphql } from 'react-apollo'
3+
import gql from 'graphql-tag'
4+
import RaisedButton from 'material-ui/RaisedButton'
5+
import Dialog from 'material-ui/Dialog'
6+
import FlatButton from 'material-ui/FlatButton'
7+
import Email from './email'
8+
9+
class SubscribeForm extends React.Component {
10+
static propTypes = {
11+
mutate: React.PropTypes.func.isRequired,
12+
}
13+
14+
constructor(props) {
15+
super(props)
16+
17+
this.onSubmit = this.onSubmit.bind(this)
18+
this.state = {
19+
open: false,
20+
error: '',
21+
}
22+
}
23+
24+
onSubmit(event) {
25+
event.preventDefault()
26+
const email = this.formEmail.state.value
27+
this.props.mutate({ variables: { email } })
28+
.then(({ data }) => {
29+
this.formEmail.setState({ value: '' })
30+
this.setState({ error: '' })
31+
this.handleOpen()
32+
})
33+
.catch((error) => {
34+
this.setState({ error: error.graphQLErrors[0].data.reason })
35+
})
36+
}
37+
38+
handleOpen = () => {
39+
this.setState({ open: true })
40+
}
41+
42+
handleClose = () => {
43+
this.setState({ open: false })
44+
}
45+
46+
render() {
47+
return (
48+
<form
49+
style={{
50+
margin: '10px 0 40px 0',
51+
display: 'flex',
52+
flexDirection: 'column',
53+
alignItems: 'center',
54+
}}
55+
onSubmit={this.onSubmit}
56+
>
57+
<div>
58+
<p>{this.state.error}</p>
59+
</div>
60+
<Email
61+
className="form-control"
62+
ref={(email) => {
63+
this.formEmail = email
64+
}}
65+
autoFocus
66+
/>
67+
<RaisedButton
68+
label="Get early access"
69+
primary
70+
style={{
71+
marginTop: 20,
72+
}}
73+
type="submit"
74+
/>
75+
<Dialog
76+
title="You will hear from us soon!"
77+
modal={false}
78+
open={this.state.open}
79+
onRequestClose={this.handleClose}
80+
/>
81+
</form>
82+
)
83+
}
84+
}
85+
86+
const subscribe = gql`
87+
mutation subscribe($email: String!) {
88+
subscribe(email: $email) {
89+
email
90+
}
91+
}
92+
`
93+
94+
export default graphql(subscribe)(SubscribeForm)

data/connectors.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Mongoose from 'mongoose'
2+
3+
const MONGO_URL = process.env.MONGO_URL || 'mongodb://localhost/graphQLGuide'
4+
5+
Mongoose.Promise = Promise
6+
7+
Mongoose.connect(MONGO_URL, {
8+
server: {
9+
auto_reconnect: true,
10+
reconnectTries: Number.MAX_VALUE,
11+
reconnectInterval: 1000,
12+
},
13+
}, (err) => {
14+
if (err) throw err
15+
})
16+
17+
Mongoose.connection.on('error', (e) => {
18+
if (e.message.code === 'ETIMEDOUT') {
19+
console.log(e)
20+
Mongoose.connect(MONGO_URL)
21+
}
22+
console.log(e)
23+
})
24+
25+
Mongoose.connection.once('open', () => {
26+
console.log(`MongoDB successfully connected to ${MONGO_URL}`)
27+
})
28+
29+
const SubscriberSchema = Mongoose.Schema({ // eslint-disable-line
30+
email: String,
31+
createdAt: Date,
32+
source: String,
33+
}, {
34+
versionKey: false,
35+
})
36+
37+
const Subscriber = Mongoose.model('subscribers', SubscriberSchema)
38+
39+
export default Subscriber

data/resolvers.js

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,49 @@
1+
import { createError } from 'apollo-errors'
2+
import Subscriber from './connectors'
3+
4+
const DbError = createError('DbError', {
5+
message: 'An error occured when saving to the database',
6+
})
7+
18
export default {
29
Query: {
3-
foo(root, args, context) {
4-
return 'bar'
10+
subscriber(root, args, context) {
11+
return Subscriber.findOne({ email: args.email })
12+
},
13+
},
14+
15+
Mutation: {
16+
async subscribe(root, args, context) {
17+
const subscriberExists = await Subscriber.findOne({ email: args.email })
18+
19+
let error
20+
21+
if (subscriberExists) {
22+
error = 'A subscriber with this email already exists'
23+
} else if (args.email.length === 0) {
24+
error = 'The email provided should not be empty'
25+
} else {
26+
const isValid = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/.test(args.email)
27+
if (!isValid) {
28+
error = 'Provide a valid email'
29+
}
30+
}
31+
32+
if (error) {
33+
throw new DbError({
34+
data: {
35+
reason: error,
36+
},
37+
})
38+
}
39+
40+
const subscriber = new Subscriber({
41+
email: args.email,
42+
createdAt: new Date(),
43+
source: 'prelaunch-landing',
44+
})
45+
46+
return subscriber.save()
547
},
648
},
749
}

data/schema.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
export default [`
2+
type Subscriber {
3+
email: String,
4+
createdAt: String,
5+
source: String,
6+
}
7+
28
type Query {
3-
foo: String
9+
subscriber (email: String!): Subscriber
10+
}
11+
12+
type Mutation {
13+
subscribe(email: String!): Subscriber
414
}
515
616
schema {
717
query: Query
18+
mutation: Mutation
819
}
920
`]

lib/initClient.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import ApolloClient, { createNetworkInterface } from 'apollo-client'
2+
3+
let apolloClient
4+
5+
function createClient(headers) {
6+
return new ApolloClient({
7+
ssrMode: !process.browser,
8+
headers,
9+
dataIdFromObject: result => result.id || null,
10+
networkInterface: createNetworkInterface({
11+
uri: '/graphql',
12+
opts: {
13+
credentials: 'same-origin',
14+
},
15+
}),
16+
})
17+
}
18+
19+
export default (headers) => {
20+
if (!process.browser) {
21+
return createClient(headers)
22+
}
23+
if (!apolloClient) {
24+
apolloClient = createClient(headers)
25+
}
26+
return apolloClient
27+
}

lib/initStore.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { createStore } from 'redux'
2+
import getReducer from './reducer'
3+
import createMiddleware from './middleware'
4+
5+
let reduxStore = null
6+
7+
export default (client, initialState) => {
8+
let store
9+
if (!process.browser || !reduxStore) {
10+
const middleware = createMiddleware(client.middleware())
11+
store = createStore(getReducer(client), initialState, middleware)
12+
if (!process.browser) {
13+
return store
14+
}
15+
reduxStore = store
16+
}
17+
return reduxStore
18+
}

lib/middleware.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { applyMiddleware, compose } from 'redux'
2+
3+
export default function createMiddleware(clientMiddleware) {
4+
const middleware = applyMiddleware(clientMiddleware)
5+
if (process.browser && window.devToolsExtension) {
6+
return compose(middleware, window.devToolsExtension())
7+
}
8+
return middleware
9+
}

lib/reducer.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { combineReducers } from 'redux'
2+
3+
export default function getReducer (client) {
4+
return combineReducers({
5+
apollo: client.reducer()
6+
})
7+
}

lib/withData.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ApolloProvider, getDataFromTree } from 'react-apollo'
2+
import React from 'react'
3+
import 'isomorphic-fetch'
4+
import initClient from './initClient'
5+
import initStore from './initStore'
6+
7+
export default Component => (
8+
class extends React.Component {
9+
static async getInitialProps(ctx) {
10+
const headers = ctx.req ? ctx.req.headers : {}
11+
const client = initClient(headers)
12+
const store = initStore(client, client.initialState)
13+
14+
const props = {
15+
url: { query: ctx.query, pathname: ctx.pathname },
16+
...await (Component.getInitialProps ? Component.getInitialProps(ctx) : {}),
17+
}
18+
19+
if (!process.browser) {
20+
const app = (
21+
<ApolloProvider client={client} store={store}>
22+
<Component {...props} />
23+
</ApolloProvider>
24+
)
25+
await getDataFromTree(app)
26+
}
27+
28+
const state = store.getState()
29+
return {
30+
initialState: {
31+
...state,
32+
apollo: {
33+
data: state.apollo.data,
34+
},
35+
},
36+
headers,
37+
...props,
38+
}
39+
}
40+
41+
constructor(props) {
42+
super(props)
43+
this.client = initClient(this.props.headers)
44+
this.store = initStore(this.client, this.props.initialState)
45+
}
46+
47+
render() {
48+
return (
49+
<ApolloProvider client={this.client} store={this.store}>
50+
<Component {...this.props} />
51+
</ApolloProvider>
52+
)
53+
}
54+
}
55+
)

0 commit comments

Comments
 (0)