Skip to content

Commit 5d9ab78

Browse files
authored
Merge pull request #15 from tutorcruncher/location-search
Location search
2 parents 6c76767 + 34ee60a commit 5d9ab78

File tree

19 files changed

+272
-148
lines changed

19 files changed

+272
-148
lines changed

public/simple/index.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
<script>
3535
var public_key = '9c79f14df986a1ec693c'
3636
var api_root = null // 'https://socket-beta.tutorcruncher.com' 'http://localhost:8000'
37-
window.socket = socket(public_key, {api_root: api_root})
37+
window.socket = socket(public_key, {
38+
api_root: api_root,
39+
// subject_filter: false,
40+
// location_input: false
41+
})
3842
</script>
3943
</html>

src/components/App.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class App extends Component {
3030

3131
get_text (name, replacements) {
3232
let s = this.props.config.messages[name]
33+
if (!s) {
34+
console.warn(`not translation found for "${name}"`)
35+
return name
36+
}
3337
for (let [k, v] of Object.entries(replacements || {})) {
3438
s = s.replace(`{${k}}`, v)
3539
}

src/components/contractors/Contractors.js

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,28 @@ import {async_start, slugify} from '../../utils'
44
import {If} from '../shared/Tools'
55
import {Grid, List} from './List'
66
import ConModal from './ConModal'
7-
import SelectSubjects from './SelectSubjects'
7+
import {SubjectSelect, LocationInput} from './Filters'
88

99
class Contractors extends Component {
1010
constructor (props) {
1111
super(props)
1212
this.state = {
13-
contractors: [],
14-
got_contractors: false,
13+
contractor_response: null,
1514
page: 1,
1615
more_pages: false,
1716
subjects: [],
1817
selected_subject: null,
1918
last_url: null,
19+
location_str: null,
2020
}
2121
this.update_contractors = this.update_contractors.bind(this)
2222
this.get_contractor_details = this.get_contractor_details.bind(this)
2323
this.set_contractor_details = this.set_contractor_details.bind(this)
2424
this.subject_url = this.subject_url.bind(this)
2525
this.page_url = this.page_url.bind(this)
2626
this.subject_change = this.subject_change.bind(this)
27+
this.location_change = this.location_change.bind(this)
28+
this.submit_location = this.submit_location.bind(this)
2729
}
2830

2931
async componentDidMount () {
@@ -59,36 +61,42 @@ class Contractors extends Component {
5961
this.update_contractors(selected_subject)
6062
}
6163

62-
async update_contractors (selected_subject) {
64+
location_change (loc) {
65+
this.setState({location_str: loc})
66+
}
67+
68+
submit_location (location_str) {
69+
this.update_contractors(this.state.selected_subject, location_str)
70+
}
71+
72+
async update_contractors (selected_subject, location_str) {
6373
if (!selected_subject) {
6474
const m = this.props.history.location.pathname.match(/subject\/(\d+)/)
6575
const subject_id = m ? parseInt(m[1], 10) : null
6676
if (subject_id && this.state.subjects.length > 0) {
6777
selected_subject = this.state.subjects.find(s => s.id === subject_id)
6878
}
6979
}
80+
if (location_str === undefined) {
81+
location_str = this.state.location_str
82+
}
7083

7184
const m = this.props.history.location.pathname.match(/page\/(\d+)/)
7285
const page = m ? parseInt(m[1], 10) : 1
7386
this.setState({selected_subject, page})
7487
const args = Object.assign({}, this.props.config.contractor_filter, {
7588
subject: selected_subject ? selected_subject.id : null,
7689
pagination: this.props.config.pagination,
90+
sort: this.props.config.sort_on,
7791
page: page,
92+
location: location_str,
7893
})
79-
const data = await this.props.root.requests.get('contractors', args)
80-
let contractors
81-
if (Array.isArray(data)) {
82-
contractors = data
83-
} else {
84-
contractors = data.results
85-
}
86-
this.props.config.event_callback('updated_contractors', contractors)
87-
this.setState({contractors: []})
94+
const contractor_response = await this.props.root.requests.get('contractors', args)
95+
this.props.config.event_callback('updated_contractors', contractor_response)
96+
this.setState({contractor_response: {results: []}})
8897
setTimeout(() => this.setState({
89-
contractors,
90-
got_contractors: true,
91-
more_pages: contractors.length === this.props.config.pagination,
98+
contractor_response,
99+
more_pages: contractor_response.count > contractor_response.results.length,
92100
}), 0)
93101
}
94102

@@ -109,20 +117,42 @@ class Contractors extends Component {
109117
}
110118

111119
render () {
120+
let description = ''
121+
const con_count = this.state.contractor_response && this.state.contractor_response.count
122+
if (con_count && this.state.selected_subject) {
123+
const msg_id_suffix = con_count === 1 ? 'single' : 'plural'
124+
description = this.props.root.get_text('subject_filter_summary_' + msg_id_suffix, {
125+
count: con_count,
126+
subject: this.state.selected_subject.name,
127+
})
128+
}
112129
const DisplayComponent = this.props.config.mode === 'grid' ? Grid : List
113130
return (
114131
<div className="tcs-app tcs-contractors">
115-
<If v={this.state.got_contractors && this.props.config.subject_filter}>
116-
<SelectSubjects get_text={this.props.root.get_text}
117-
contractors={this.state.contractors}
118-
subjects={this.state.subjects}
119-
selected_subject={this.state.selected_subject}
120-
subject_change={this.subject_change}/>
132+
<If v={this.state.contractor_response}>
133+
<div className="tcs-filters-container">
134+
<LocationInput get_text={this.props.root.get_text}
135+
show={this.props.config.show_location_search}
136+
loc_raw={this.state.location_str}
137+
loc_change={this.location_change}
138+
submit={this.submit_location}/>
139+
140+
<SubjectSelect get_text={this.props.root.get_text}
141+
show={this.props.config.show_subject_filter}
142+
subjects={this.state.subjects}
143+
selected_subject={this.state.selected_subject}
144+
subject_change={this.subject_change}/>
145+
</div>
146+
<div key="summary" className="tcs-summary">
147+
{description}
148+
</div>
121149
</If>
122-
<DisplayComponent contractors={this.state.contractors} root={this.props.root}/>
123-
<If v={this.state.got_contractors && this.state.contractors.length === 0}>
150+
<DisplayComponent
151+
contractors={this.state.contractor_response ? this.state.contractor_response.results : []}
152+
root={this.props.root}/>
153+
<If v={this.state.contractor_response && this.state.contractor_response.count === 0}>
124154
<div className="tcs-no-contractors">
125-
{this.props.root.get_text('no_tutors_found')}
155+
{this.props.root.get_text(this.state.location_str === null ? 'no_tutors_found' : 'no_tutors_found_loc')}
126156
</div>
127157
</If>
128158

@@ -145,8 +175,8 @@ class Contractors extends Component {
145175
<Route path={this.props.root.url(':id(\\d+):_extra')} render={props => (
146176
<ConModal id={props.match.params.id}
147177
last_url={this.state.last_url}
148-
contractors={this.state.contractors}
149-
got_contractors={this.state.got_contractors}
178+
contractors={this.state.contractor_response.results}
179+
got_contractors={!!this.state.contractor_response}
150180
get_contractor_details={this.get_contractor_details}
151181
root={this.props.root}
152182
config={this.props.config}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react'
2+
import Select from 'react-select'
3+
import 'react-select/dist/react-select.css'
4+
import {If} from '../shared/Tools'
5+
6+
export const SubjectSelect = ({get_text, show, subjects, selected_subject, subject_change}) => {
7+
return (
8+
<div className="tcs-contractor-filter">
9+
<If v={show}>
10+
<Select
11+
value={selected_subject && selected_subject.id}
12+
onChange={subject_change}
13+
placeholder={get_text('subject_filter_placeholder')}
14+
labelKey='name'
15+
valueKey='id'
16+
options={subjects}/>
17+
</If>
18+
</div>
19+
)
20+
}
21+
22+
23+
export const LocationInput = ({get_text, show, loc_raw, loc_change, submit}) => {
24+
return (
25+
<div className="tcs-contractor-filter">
26+
<If v={show}>
27+
<div className="tcs-location-filter">
28+
<input className="tcs-location-input"
29+
type="text"
30+
value={loc_raw || ''}
31+
onChange={v => loc_change(v.target.value || null)}
32+
onKeyPress={v => v.key === 'Enter' && submit()}
33+
placeholder={get_text('location_input_placeholder')}/>
34+
<span className="tcs-location-clear"
35+
style={{visibility: loc_raw === null ? 'hidden' : 'visible'}}
36+
onClick={() => loc_change(null) || submit(null)}>
37+
×
38+
</span>
39+
</div>
40+
</If>
41+
</div>
42+
)
43+
}
44+

src/components/contractors/List.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { Component } from 'react'
22
import {Link} from 'react-router-dom'
3-
import {Location, Markdown} from '../shared/Tools'
3+
import {Location, Markdown, If} from '../shared/Tools'
44
import Stars from './Stars'
55

66
class AnimateLink extends Component {
@@ -64,6 +64,11 @@ export const List = ({contractors, root}) => (
6464
<div className="tcs-location">
6565
<Location/>
6666
<span>{contractor.town}</span>
67+
<div className="tcs-distance">
68+
<If v={contractor.distance !== null}>
69+
{root.get_text('distance_away', {distance: Math.round(contractor.distance / 100) / 10})}
70+
</If>
71+
</div>
6772
</div>
6873
</div>
6974
</AnimateLink>

src/components/contractors/SelectSubjects.js

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

src/conf.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ $size-sm: 768px;
1818
$button-colour: $highlight;
1919
// size used for images in grid and list view
2020
$dft-image-size: 150px;
21+
$grey-text: #888;

src/index.js

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,17 @@ const STRINGS = {
3434
enquiry_title: 'Enquiry',
3535
grecaptcha_missing: 'This captcha is required',
3636
required: ' (Required)',
37-
subject_filter: 'Filter by subject',
37+
subject_filter_placeholder: 'Select a subject...',
3838
subject_filter_summary_single: '{subject}: showing 1 result',
3939
subject_filter_summary_plural: '{subject}: showing {count} results',
40+
location_input_placeholder: 'Enter your address or zip/postal code...',
4041
view_profile: 'View Profile',
4142
review_hours: '({hours} hours)',
4243
previous: 'Previous',
4344
next: 'Next',
4445
no_tutors_found: 'No more tutors found',
46+
no_tutors_found_loc: 'No more tutors found near this location',
47+
distance_away: '{distance}km away',
4548
}
4649

4750
const MODES = ['grid', 'list', 'enquiry', 'enquiry-modal']
@@ -66,11 +69,8 @@ window.socket = async function (public_key, config) {
6669
}
6770
}
6871

69-
let options_required = false
7072
let error = null
71-
if (!config.mode) {
72-
options_required = true
73-
} else if (MODES.indexOf(config.mode) === -1) {
73+
if (config.mode && MODES.indexOf(config.mode) === -1) {
7474
error = `invalid mode "${config.mode}", options are: ${MODES.join(', ')}`
7575
config.mode = 'grid'
7676
}
@@ -94,8 +94,6 @@ window.socket = async function (public_key, config) {
9494
// use history mode with enquiry so it doesn't add the hash
9595
if (config.mode === 'enquiry') {
9696
config.router_mode = 'history'
97-
} else {
98-
options_required = true
9997
}
10098
} else if (ROUTER_MODES.indexOf(config.router_mode) === -1) {
10199
error = `invalid router mode "${config.router_mode}", options are: ${ROUTER_MODES.join(', ')}`
@@ -116,10 +114,6 @@ window.socket = async function (public_key, config) {
116114
delete config.labels_exclude
117115
}
118116

119-
if (config.subject_filter === undefined) {
120-
config.subject_filter = true
121-
}
122-
123117
if (!config.event_callback) {
124118
config.event_callback = () => null
125119
}
@@ -130,7 +124,6 @@ window.socket = async function (public_key, config) {
130124
return
131125
}
132126

133-
config.pagination = config.pagination || 100
134127
config.messages = config.messages || {}
135128
for (let k of Object.keys(STRINGS)) {
136129
if (!config.messages[k]) {
@@ -140,20 +133,31 @@ window.socket = async function (public_key, config) {
140133
config.random_id = Math.random().toString(36).substring(2, 10)
141134
config.grecaptcha_key = process.env.REACT_APP_GRECAPTCHA_KEY
142135

143-
if (options_required) {
144-
let company_options
145-
try {
146-
company_options = await get_company_options(public_key, config)
147-
} catch(e) {
148-
error = e.toString()
149-
company_options = {
150-
display_mode: 'grid',
151-
router_mode: 'hash',
152-
}
136+
let company_options
137+
try {
138+
company_options = await get_company_options(public_key, config)
139+
} catch(e) {
140+
error = e.toString()
141+
// these are the default values
142+
company_options = {
143+
display_mode: 'grid',
144+
pagination: 100,
145+
router_mode: 'hash',
146+
show_hours_reviewed: true,
147+
show_labels: true,
148+
show_location_search: true,
149+
show_stars: true,
150+
show_subject_filter: true,
151+
sort_on: 'name',
152+
}
153+
}
154+
155+
company_options.mode = company_options.display_mode
156+
console.debug('company options:', company_options)
157+
for (let [k, v] of Object.entries(company_options)) {
158+
if (config[k] === undefined) {
159+
config[k] = v
153160
}
154-
console.debug('company options:', company_options)
155-
config.mode = config.mode || company_options.display_mode
156-
config.router_mode = config.router_mode || company_options.router_mode
157161
}
158162

159163
console.debug('using config:', config)

src/main.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
@import './styles/stars';
2424
@import './styles/tools';
2525
@import './styles/contractors';
26+
@import './styles/contractor-filters';
2627
@import './styles/grid';
2728
@import './styles/list';
2829
@import './styles/input';
2930
@import './styles/modal';
30-
@import './styles/subject-select';

0 commit comments

Comments
 (0)