Skip to content

Commit d1c9531

Browse files
Merge pull request #20 from tutorcruncher/appointments
Appointments
2 parents 1ad45bd + ead9459 commit d1c9531

35 files changed

+1387
-469
lines changed

config-overrides.js

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@ module.exports = function override (config, env) {
1212
// https://github.com/facebookincubator/create-react-app/issues/2498
1313
config.module.rules[1].oneOf.splice(0, 0,
1414
{
15-
test: /\.(sass|scss|css)$/,
16-
use: [
17-
'style-loader',
18-
'css-loader',
19-
{
20-
loader: 'sass-loader',
21-
options: {
22-
outputStyle: 'compressed',
23-
includePaths: [path.resolve(__dirname, 'node_modules')],
24-
}
15+
test: /\.(sass|scss|css)$/,
16+
use: [
17+
'style-loader',
18+
'css-loader',
19+
{
20+
loader: 'sass-loader',
21+
options: {
22+
outputStyle: 'compressed',
23+
includePaths: [path.resolve(__dirname, 'node_modules')],
2524
}
26-
]
25+
}
26+
]
2727
},
2828
)
2929
if (env === 'production') {
@@ -41,14 +41,21 @@ module.exports = function override (config, env) {
4141
)
4242
}
4343
}
44-
// add another output file at /simple/
44+
// add another output file at /simple/ and /appointments/
4545
config.plugins.splice(2, 0,
4646
new HtmlWebpackPlugin({
4747
inject: true,
4848
template: path.resolve(__dirname, 'public', 'simple/index.html'),
4949
filename: 'simple/index.html'
5050
})
5151
)
52+
config.plugins.splice(2, 0,
53+
new HtmlWebpackPlugin({
54+
inject: true,
55+
template: path.resolve(__dirname, 'public', 'appointments/index.html'),
56+
filename: 'appointments/index.html'
57+
})
58+
)
5259
// console.dir(config, { depth: 10, colors: true })
5360
return config
5461
}

public/appointments/index.html

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6+
<title>tutorcruncher-socket</title>
7+
<style>
8+
@import url(https://fonts.googleapis.com/css?family=Open+Sans:400,500);
9+
body {
10+
background: #eee;
11+
font-family: 'Open Sans', sans-serif;
12+
}
13+
* {
14+
box-sizing: border-box; /* bootstrap does this on most sites. */
15+
}
16+
main {
17+
max-width: 800px;
18+
margin: 20px auto;
19+
}
20+
main > div {
21+
background-color: white;
22+
border: 1px solid #aaa;
23+
}
24+
</style>
25+
</head>
26+
<body>
27+
<main>
28+
<p><a href="https://github.com/tutorcruncher/socket-frontend">github.com/tutorcruncher/socket-frontend</a></p>
29+
<p><a href="/">back to index</a></p>
30+
<p>appointments socket view:</p>
31+
<div id="socket"></div>
32+
</main>
33+
</body>
34+
<script>
35+
var public_key = '9c79f14df986a1ec693c'
36+
var api_root = null // 'https://socket-beta.tutorcruncher.com' 'http://localhost:8000'
37+
window.socket = socket(public_key, {
38+
mode: 'appointments',
39+
api_root: api_root,
40+
})
41+
</script>
42+
</html>

public/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<main>
2828
<p><a href="https://github.com/tutorcruncher/socket-frontend">github.com/tutorcruncher/socket-frontend</a></p>
2929
<p><a href="/simple/">view simple socket panel</a></p>
30+
<p><a href="/appointments/">view appointments socket panel</a></p>
3031
<p>mode defined by options:</p>
3132
<div id="socket1"></div>
3233
<p>Grid mode:</p>

src/components/App.js

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React, { Component } from 'react'
22
import {withRouter} from 'react-router-dom'
3-
import {google_analytics, requests, async_start} from '../utils'
3+
import {google_analytics, request} from '../utils'
44
import Error from './shared/Error'
55
import Contractors from './contractors/Contractors'
66
import PlainEnquiry from './enquiry/PlainEnquiry'
77
import EnquiryButton from './enquiry/EnquiryButton'
8+
import Appointments from './appointments/Appointments'
89

910
class App extends Component {
1011
constructor (props) {
@@ -15,34 +16,27 @@ class App extends Component {
1516
enquiry_form_info: null,
1617
}
1718
this.url = props.url_generator
18-
this.get_text = this.get_text.bind(this)
19-
20-
this.get_enquiry = this.get_enquiry.bind(this)
21-
this.set_enquiry = this.set_enquiry.bind(this)
2219

2320
this.ga_event = this.ga_event.bind(this)
21+
this.request = request.bind(this)
2422
this.requests = {
25-
get: async (...args) => requests.get(this, ...args),
26-
post: async (...args) => requests.post(this, ...args),
27-
}
28-
}
29-
30-
31-
get_text (name, replacements) {
32-
let s = this.props.config.messages[name]
33-
if (!s) {
34-
console.warn(`not translation found for "${name}"`)
35-
return name
36-
}
37-
for (let [k, v] of Object.entries(replacements || {})) {
38-
s = s.replace(`{${k}}`, v)
23+
get: (path, args, config) => {
24+
config = config || {}
25+
config.args = args
26+
return this.request('GET', path, config)
27+
},
28+
post: (path, data, config) => {
29+
config = config || {}
30+
config.send_data = data
31+
config.expected_statuses = config.expected_statuses || [201]
32+
return this.request('POST', path, config)
33+
}
3934
}
40-
return s
4135
}
4236

4337
get_enquiry () {
4438
if (this.state.enquiry_form_info === null) {
45-
async_start(this.set_enquiry)
39+
this.set_enquiry()
4640
}
4741
return this.state.enquiry_form_info || {}
4842
}
@@ -69,6 +63,8 @@ class App extends Component {
6963
return <PlainEnquiry root={this} config={this.props.config}/>
7064
} else if (this.props.config.mode === 'enquiry-modal') {
7165
return <EnquiryButton root={this} config={this.props.config}/>
66+
} else if (this.props.config.mode === 'appointments') {
67+
return <Appointments root={this} config={this.props.config} history={this.props.history}/>
7268
} else {
7369
// grid or list
7470
return <Contractors root={this} config={this.props.config} history={this.props.history}/>
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import React, { Component } from 'react'
2+
import {Link, Route} from 'react-router-dom'
3+
import {colour_contrast, group_by} from '../../utils'
4+
import {If, Bull} from '../shared/Tools'
5+
import AptModal from './AptModal'
6+
7+
const LS_KEY = '_tcs_user_data_'
8+
9+
const group_appointments = apts => {
10+
// group appointments by month then day
11+
return group_by(apts, a => a.start.substr(0, 7))
12+
.map(apts_ => ({
13+
date: apts_[0].start,
14+
appointments: group_by(apts_, a => a.start.substr(0, 10))
15+
}))
16+
}
17+
18+
const Apt = ({apt, props, appointment_attendees}) => {
19+
let status
20+
const spaces_ctx = {spaces: apt.attendees_max === null ? null : apt.attendees_max - apt.attendees_count}
21+
if (appointment_attendees && appointment_attendees[apt.id] !== undefined) {
22+
status = props.config.get_text('spaces_attending', spaces_ctx)
23+
} else {
24+
status = props.config.get_text('spaces', spaces_ctx)
25+
}
26+
return (
27+
<Link to={props.root.url(`appointment/${apt.link}`)} className="tcs-item">
28+
<div className={`tcs-apt ${colour_contrast(apt.service_colour)}`} style={{background: apt.service_colour}}>
29+
<div style={{fontWeight: 700, marginRight: 8, minWidth: 68}}>
30+
{props.config.format_dt(apt.start, 'time')}
31+
</div>
32+
<div className="tcs-truncate">
33+
{apt.topic}<Bull/>{apt.service_name}
34+
</div>
35+
<div className="tcs-right" style={{fontWeight: 700}}>
36+
{props.config.format_money(apt.price)}
37+
</div>
38+
<div className="tcs-truncate" style={{gridColumn: 2}}>
39+
{status}
40+
</div>
41+
<div className="tcs-right">
42+
{props.config.format_duration(apt.finish, apt.start)}
43+
</div>
44+
</div>
45+
</Link>
46+
)
47+
}
48+
49+
const AptDayGroup = ({appointments, props, appointment_attendees}) => {
50+
const first_apt = appointments[0]
51+
return (
52+
<div className="tcs-apt-group-day">
53+
<div className="tcs-day">
54+
{props.config.format_dt(first_apt.start, 'weekday')}
55+
<div className="tcs-day-no">{props.config.format_dt(first_apt.start, 'day')}</div>
56+
</div>
57+
<div>
58+
{appointments.map(apt => (
59+
<Apt key={apt.id} apt={apt} props={props} appointment_attendees={appointment_attendees}/>
60+
))}
61+
</div>
62+
</div>
63+
)
64+
}
65+
66+
class Appointments extends Component {
67+
constructor (props) {
68+
super(props)
69+
this.state = {
70+
appointments: null,
71+
page: 1,
72+
more_pages: false,
73+
display_data: null,
74+
appointment_attendees: null,
75+
}
76+
this.sso_args = null
77+
this.update = this.update.bind(this)
78+
this.signin = this.signin.bind(this)
79+
this.signout = this.signout.bind(this)
80+
this.root_url = this.props.root.url('')
81+
}
82+
83+
componentDidMount () {
84+
this.update_display_data()
85+
this.update()
86+
}
87+
88+
page_url (page) {
89+
page = page || this.state.page
90+
let url = this.root_url
91+
if (page > 1) {
92+
url += `${url.substr(-1) === '/' ? '' : '/'}page/${page}`
93+
}
94+
return url
95+
}
96+
97+
async update () {
98+
const mp = this.props.history.location.pathname.match(/page\/(\d+)/)
99+
const page = mp ? parseInt(mp[1], 10) : 1
100+
const appointments = await this.props.root.requests.get('appointments', {
101+
page, pagination: this.props.config.pagination,
102+
})
103+
this.props.config.event_callback('updated_appointments', appointments)
104+
const on_previous_pages = (page - 1) * this.props.config.pagination
105+
this.sso_args && await this.update_attendees()
106+
this.setState({
107+
appointments, page,
108+
more_pages: appointments.count > appointments.results.length + on_previous_pages,
109+
})
110+
}
111+
112+
signin () {
113+
const process_message = event => {
114+
try {
115+
JSON.parse(event.data)
116+
} catch (e) {
117+
return
118+
}
119+
event.source.close()
120+
window.sessionStorage[LS_KEY] = event.data
121+
this.update_display_data()
122+
this.update_attendees()
123+
}
124+
window.addEventListener('message', process_message, false)
125+
window.open(
126+
`${this.props.config.auth_url}?site=${encodeURIComponent(window.location.href)}`,
127+
'Auth',
128+
'width=1000,height=700,left=100,top=100,scrollbars,toolbar=0,resizable'
129+
)
130+
}
131+
132+
signout () {
133+
this.setState({
134+
display_data: null,
135+
appointment_attendees: null,
136+
})
137+
this.sso_args = null
138+
window.sessionStorage.removeItem(LS_KEY)
139+
}
140+
141+
update_display_data () {
142+
const raw_data = window.sessionStorage[LS_KEY]
143+
if (raw_data) {
144+
this.sso_args = JSON.parse(raw_data)
145+
this.setState({display_data: JSON.parse(this.sso_args.sso_data)})
146+
}
147+
}
148+
149+
async update_attendees () {
150+
try {
151+
const r = await this.props.root.requests.get('check-client', this.sso_args, {set_app_state: false})
152+
this.setState({appointment_attendees: r.appointment_attendees})
153+
} catch (e) {
154+
if (e.xhr.status === 401) {
155+
this.signout()
156+
} else {
157+
this.props.root.setState({error: e.msg})
158+
}
159+
}
160+
}
161+
162+
render () {
163+
const appointments = this.state.appointments ? this.state.appointments.results : []
164+
const months = group_appointments(appointments)
165+
return (
166+
<div className="tcs-app tcs-appointments">
167+
{months.map(({date, appointments}, i) => (
168+
<div className="tcs-apt-group-month" key={i}>
169+
<div className="tcs-title">{this.props.config.format_dt(date, 'month')}</div>
170+
{appointments.map((appointments, j) => (
171+
<AptDayGroup key={j} appointments={appointments} props={this.props}
172+
appointment_attendees={this.state.appointment_attendees}/>
173+
))}
174+
</div>
175+
))}
176+
177+
<If v={this.state.page > 1 || this.state.more_pages}>
178+
<div className="tcs-pagination">
179+
<Link
180+
to={this.page_url(this.state.page - 1)}
181+
onClick={() => setTimeout(() => this.update(), 0)}
182+
className={'tcs-previous' + (this.state.page > 1 ? '' : ' tcs-disable')}>
183+
&lsaquo;&lsaquo; {this.props.config.get_text('previous')}
184+
</Link>
185+
<Link
186+
to={this.page_url(this.state.page + 1)}
187+
onClick={() => setTimeout(() => this.update(), 0)}
188+
className={'tcs-next' + (this.state.more_pages ? '' : ' tcs-disable')}>
189+
{this.props.config.get_text('next')} &rsaquo;&rsaquo;
190+
</Link>
191+
</div>
192+
</If>
193+
<Route path={this.props.root.url('appointment/:id(\\d+):_extra')} render={props => (
194+
<AptModal id={props.match.params.id}
195+
last_url={this.root_url}
196+
appointments={this.state.appointments && this.state.appointments.results}
197+
got_data={Boolean(this.state.appointments)}
198+
display_data={this.state.display_data}
199+
appointment_attendees={this.state.appointment_attendees}
200+
sso_args={this.sso_args}
201+
root={this.props.root}
202+
config={this.props.config}
203+
update={this.update}
204+
signin={this.signin}
205+
signout={this.signout}
206+
history={props.history}/>
207+
)}/>
208+
</div>
209+
)
210+
}
211+
}
212+
213+
export default Appointments

0 commit comments

Comments
 (0)