Skip to content

Commit f17e7eb

Browse files
author
Dave Newman
committed
Add additional fields and validation to new job form
1 parent 3ed9838 commit f17e7eb

File tree

7 files changed

+180
-41
lines changed

7 files changed

+180
-41
lines changed

app/assets/javascripts/components/Heart.es6.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ class Heart extends React.Component {
99
if (this.props.layout === 'inline') {
1010
classes = {
1111
root: 'heart no-hover font-x-lg',
12-
icon: 'purple',
13-
count: 'ml1 diminish bold',
12+
icon: 'inline purple',
13+
count: 'inline ml1 diminish bold',
1414
inline: 'inline'
1515
}
1616
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
const requiredFields = [
2+
'authorEmail',
3+
'authorName',
4+
'company',
5+
'companyLogo',
6+
'companyUrl',
7+
'location',
8+
'source',
9+
'title',
10+
]
11+
12+
class NewJob extends React.Component {
13+
constructor(props) {
14+
super(props)
15+
this.state = { brokenFields: {} }
16+
this.checkout = StripeCheckout.configure({
17+
key: props.stripePublishable,
18+
image: 'https://s3.amazonaws.com/stripe-uploads/A6CJ1PO8BNz85yiZbRZwpGOSsJc5yDvKmerchant-icon-356788-cwlogo.png',
19+
locale: 'auto',
20+
token: token => {
21+
console.log(this.refs)
22+
this.setState({ saving: true, stripeToken: token.id }, () => this.refs.form.getDOMNode().submit())
23+
}
24+
});
25+
}
26+
27+
componentDidMount() {
28+
$(window).on('popstate', function() {
29+
this.checkout.close()
30+
})
31+
}
32+
33+
render() {
34+
const csrfToken = document.getElementsByName('csrf-token')[0].content
35+
36+
return (
37+
<div>
38+
<h2>Let's find awesome candidates!</h2>
39+
<form ref="form" action="/jobs" acceptCharset="UTF-8" method="post" onSubmit={e => this.handleSubmit(e)}>
40+
<input name="utf8" type="hidden" defaultValue="✓" />
41+
<input type="hidden" name="authenticity_token" defaultValue={csrfToken} />
42+
<input type="hidden" name="stripeToken" value={this.state.stripeToken} />
43+
44+
<label htmlFor="job_title">Job Title</label>
45+
<input id="job_title" value={this.state.title} onChange={e => this.handleChange('title', e)} type="text" className={this.fieldClasses('title')} name="job[title]" placeholder="Senior Anvil Operator" />
46+
47+
<label htmlFor="job_company">Company Name</label>
48+
<input id="job_company" value={this.state.company} onChange={e => this.handleChange('company', e)} type="text" className={this.fieldClasses('company')} name="job[company]" placeholder="Acme Inc" />
49+
50+
<label htmlFor="job_location">Location</label>
51+
<input id="job_location" value={this.state.location} onChange={e => this.handleChange('location', e)} type="text" className={this.fieldClasses('location')} name="job[location]" placeholder="Grand Canyon" />
52+
53+
<label htmlFor="job_source">URL to your job description</label>
54+
<input id="job_source" value={this.state.source} onChange={e => this.handleChange('source', e)} type="text" className={this.fieldClasses('source')} name="job[source]" placeholder="https://acme.inc/jobs/78" />
55+
56+
<label htmlFor="job_company_url">Company URL</label>
57+
<input id="job_company_url" value={this.state.companyUrl} onChange={e => this.handleChange('companyUrl', e)} type="text" className={this.fieldClasses('companyUrl')} name="job[company_url]" placeholder="https://acme.inc" />
58+
59+
<div className="clearfix">
60+
<div className="col col-8">
61+
<label htmlFor="job_company_logo">Company Logo</label>
62+
<input id="job_company_logo" value={this.state.companyLogo} onChange={e => this.handleChange('companyLogo', e)} type="text" className={this.fieldClasses('companyLogo')} name="job[company_logo]" placeholder="https://acme.inc/logo.png" />
63+
</div>
64+
65+
<div className="col col-4 px2">
66+
<img src={this.state.validLogoUrl} />
67+
</div>
68+
</div>
69+
70+
<label htmlFor="job_author_name">Contact Name</label>
71+
<input id="job_author_name" value={this.state.authorName} onChange={e => this.handleChange('authorName', e)} type="text" className={this.fieldClasses('authorName')} name="job[author_name]" placeholder="Wile E. Coyote" />
72+
73+
<label htmlFor="job_author_email">Contact Email</label>
74+
<input id="job_author_email" value={this.state.authorEmail} onChange={e => this.handleChange('authorEmail', e)} type="email" className={this.fieldClasses('authorEmail')} name="job[author_email]" placeholder="wcoyote@acme.inc" />
75+
76+
<div className="col-12">
77+
<input id="role_type_full_time" name="age" type="radio" value="Full Time" defaultChecked />
78+
<label htmlFor="role_type_full_time">Full Time</label>
79+
<input id="role_type_part_time" name="age" type="radio" value="Part Time" />
80+
<label htmlFor="role_type_part_time">Part Time</label>
81+
</div>
82+
83+
<div>
84+
<button className="btn rounded mt1 bg-green white" type="submit" disabled={this.state.saving}>Purchase</button>
85+
</div>
86+
</form>
87+
88+
<div className="clearfix mt2">
89+
<a href="/jobs">Cancel</a>
90+
</div>
91+
</div>
92+
)
93+
}
94+
95+
fieldClasses(field) {
96+
return `field block col-12 mb3 ${this.state.brokenFields[field] && 'is-error'}`
97+
}
98+
99+
handleSubmit(e) {
100+
console.log('ON SUBMIHT')
101+
e.preventDefault()
102+
103+
let brokenFields = requiredFields.filter(f => !this.state[f])
104+
if (!this.state.validLogoUrl) {
105+
brokenFields = [...brokenFields, 'companyLogo']
106+
}
107+
this.setState({ brokenFields: brokenFields.reduce((memo, i) => ({...memo, [i]: true}), {}) })
108+
// if (brokenFields.length > 0) { return }
109+
110+
this.checkout.open({
111+
name: "Jobs @ coderwall.com",
112+
description: "30 day listing",
113+
amount: this.props.cost,
114+
})
115+
}
116+
117+
handleChange(input, e) {
118+
this.setState({[input]: e.target.value})
119+
120+
if (input === 'companyLogo') {
121+
this.testImage(e.target.value, (url, result) => {
122+
if (result === 'success') {
123+
this.setState({ validLogoUrl: url})
124+
} else {
125+
this.setState({ validLogoUrl: null })
126+
}
127+
})
128+
}
129+
}
130+
131+
testImage(url, callback, timeout) {
132+
timeout = timeout || 5000
133+
var timedOut = false, timer
134+
var img = new Image()
135+
img.onerror = img.onabort = function() {
136+
if (!timedOut) {
137+
clearTimeout(timer)
138+
callback(url, "error");
139+
}
140+
}
141+
img.onload = function() {
142+
if (!timedOut) {
143+
clearTimeout(timer)
144+
callback(url, "success")
145+
}
146+
}
147+
img.src = url
148+
timer = setTimeout(function() {
149+
timedOut = true
150+
callback(url, "timeout")
151+
}, timeout)
152+
}
153+
}
154+
155+
156+
NewJob.propTypes = {
157+
cost: React.PropTypes.number.isRequired,
158+
stripePublishable: React.PropTypes.string.isRequired
159+
}

app/assets/stylesheets/application.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ header {
174174
}
175175

176176
//neutralize rails-react wrappers affect on layout
177-
div[data-react-class], div[data-reactid] {
177+
div[data-react-class] {
178178
display: inline;
179179
}
180180

app/controllers/jobs_controller.rb

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,12 @@ def show
2727
def create
2828
@job = Job.new(job_params)
2929
if @job.save
30-
redirect_to review_job_path(@job)
30+
redirect_to jobs_path(posted: @job.id)
3131
else
3232
render action: 'new'
3333
end
3434
end
3535

36-
def review
37-
@job = Job.find(params[:id])
38-
end
39-
4036
def publish
4137
@job = Job.find(params[:job_id])
4238
@job.charge!(params['stripeToken'])
@@ -46,13 +42,23 @@ def publish
4642

4743
rescue Stripe::CardError => e
4844
flash[:notice] = e.message
49-
redirect_to review_job_path(@job)
45+
redirect_to new_job_path(@job)
5046
end
5147

5248
# private
5349

5450
def job_params
55-
params.require(:job).permit(:source)
51+
params.require(:job).permit(
52+
:author_email,
53+
:author_name,
54+
:company_logo,
55+
:company_url,
56+
:company,
57+
:location,
58+
:role_type,
59+
:source,
60+
:title
61+
)
5662
end
5763

5864
end

app/views/jobs/new.html.haml

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
.contsiner
1+
%script(src="https://checkout.stripe.com/checkout.js")
2+
3+
.container
24
.clearfix
5+
.md-col.md-col-2.md-show &nbsp;
36
.sm-col.sm-col.sm-col-12.md-col-8
47
.card.p3
5-
%h2 Let's find awesome candidates!
6-
7-
= form_for @job do |form|
8-
= form.label :source, 'Job URL'
9-
= form.text_field :source, type: 'text', class: 'field block col-12 mb3'
10-
11-
%button.btn.rounded.mt1.bg-green.white{type: 'submit'} Save & Review
12-
13-
.clearfix.mt2
14-
=link_to 'Cancel', :back
8+
= react_component 'NewJob', stripePublishable: ENV['STRIPE_PUBLISHABLE_KEY'], cost: Job::CENTS_PER_MONTH

app/views/jobs/review.html.haml

Lines changed: 0 additions & 19 deletions
This file was deleted.

config/routes.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
Rails.application.routes.draw do
22
resources :jobs do
3-
get :review, on: :member
43
post :publish
54
end
65

0 commit comments

Comments
 (0)