Newer
Older
import Hero from '../components/Hero.jsx'
import './Exam.css'
import GeneratedExamPreview from '../components/GeneratedExamPreview.jsx'
import PanelGenerate from '../components/PanelGenerate.jsx'
import ExamEditor from './ExamEditor.jsx'
import update from 'immutability-helper'
import ExamFinalizeMarkdown from './ExamFinalize.md'
import * as api from '../api.jsx'
class Exams extends React.Component {

Thomas Roos
committed
state = {
examID: null,
page: 0,
numPages: null,
selectedWidgetId: null,
widgets: [],
previewing: false
}

Thomas Roos
committed
static getDerivedStateFromProps = (newProps, prevState) => {
if (newProps.exam.id !== prevState.examID) {
// initialize array to size of pdf
const widgets = []
newProps.exam.problems.forEach(problem => {
// keep page and name of problem as widget.problem object
widgets[problem.widget.id] = {
...problem.widget,
problem: {
id: problem.id,
page: problem.page,
name: problem.name

Thomas Roos
committed
})

Thomas Roos
committed
newProps.exam.widgets.forEach(examWidget => {
widgets[examWidget.id] = examWidget
})

Thomas Roos
committed
return {
examID: newProps.exam.id,
page: 0,
numPages: null,
selectedWidgetId: null,
widgets: widgets,
previewing: false

Thomas Roos
committed
return null
}

Thomas Roos
committed
componentDidUpdate = (prevProps) => {
if (prevProps.examID !== this.props.examID) {
this.props.updateExam(this.props.examID)

Thomas Roos
committed
}

Thomas Roos
committed
componentDidMount = () => {
if (this.props.examID !== this.props.exam.id) this.props.updateExam(this.props.examID)
}
componentWillUnmount = () => {
// This might save the name unnecessary, but better twice than never.
// We could keep track in state if we need to save the name when the double requests cause issues
this.saveProblemName()
}
saveProblemName = () => {
const selectedWidgetId = this.state.selectedWidgetId
if (!selectedWidgetId) return
const selectedWidget = this.state.widgets[selectedWidgetId]
if (!selectedWidget) return
const problem = selectedWidget.problem
if (!problem) return
api.put('problems/' + problem.id + '/name', { name: problem.name })
.catch(e => alert('Could not save new problem name: ' + e))
}

Thomas Roos
committed
deleteWidget = (widgetId, prompt = true) => {
const widget = this.state.widgets[widgetId]
if (widget) {
if (prompt && confirm('Are you sure you want to delete this widget?')) {
api.del('problems/' + widget.problem.id)
.then(() => {
this.setState((prevState) => {
return {
selectedWidgetId: null,
widgets: update(prevState.widgets, {

Thomas Roos
committed
})
}
})
})
.catch(err => {
console.log(err)
// update to try and get a consistent state
this.updateExam()
})

Thomas Roos
committed
}

Thomas Roos
committed
updateWidget = (widgetId, newData) => {
this.setState(prevState => ({
widgets: update(prevState.widgets, {
[widgetId]: newData

Thomas Roos
committed
}))
}

Thomas Roos
committed
renderContent = () => {
if (this.state.previewing) {

Thomas Roos
committed
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
<GeneratedExamPreview
examID={this.state.examID}
page={this.state.page}
onPDFLoad={this.onPDFLoad}
/>
)
} else {
return (
<ExamEditor
finalized={this.props.exam.finalized}
widgets={this.state.widgets}
examID={this.state.examID}
page={this.state.page}
numPages={this.state.numPages}
onPDFLoad={this.onPDFLoad}
updateWidget={this.updateWidget}
selectedWidgetId={this.state.selectedWidgetId}
selectWidget={(widgetId) => {
this.setState({
selectedWidgetId: widgetId
})
}}
createNewWidget={(widgetData) => {
this.setState((prevState) => {
return {
selectedWidgetId: widgetData.id,

Thomas Roos
committed
[widgetData.id]: {
$set: widgetData

Thomas Roos
committed
}
})
}}
/>

Thomas Roos
committed
}

Thomas Roos
committed
Pager = (props) => {
const isDisabled = props.numPages == null
const pageNum = isDisabled ? '_' : props.page + 1
const numPages = isDisabled ? '_' : props.numPages
return (
<div className='field has-addons is-mobile'>
<div className='control'>
<button
disabled={isDisabled}
type='submit'
className='button is-link is-rounded'
onClick={() => props.setPage(props.page - 1)}
>
Previous
</button>
</div>
<div className='control'>
<div className='field-text is-rounded has-text-centered is-link'>
{'Page ' + pageNum + ' of ' + numPages}

Thomas Roos
committed
</div>
<div className='control'>
<button
disabled={isDisabled}
type='submit'
className='button is-link is-rounded'
onClick={() => props.setPage(props.page + 1)}
>
Next
</button>
</div>
</div>
)
}

Thomas Roos
committed
onPDFLoad = (pdf) => {
this.setState((newProps, prevState) => ({
numPages: pdf.numPages
}), () => {
this.props.updateExam(this.props.examID)
})
}

Thomas Roos
committed
setPage = (newPage) => {
this.setState((prevState) => {
return {
// clamp the page
selectedWidgetId: null,
page: Math.max(0, Math.min(newPage, prevState.numPages - 1))

Thomas Roos
committed
})
}

Thomas Roos
committed
SidePanel = (props) => {
const selectedWidgetId = this.state.selectedWidgetId
let selectedWidget = selectedWidgetId && this.state.widgets[selectedWidgetId]
let problem = selectedWidget && selectedWidget.problem
let widgetEditDisabled = this.state.previewing || !problem
return (
<React.Fragment>
<this.PanelEdit
disabled={widgetEditDisabled}
onDeleteClick={() => {
this.deleteWidget(selectedWidgetId)
}}
problem={problem}
changeProblemName={newName => {
this.setState(prevState => ({
widgets: update(prevState.widgets, {
[selectedWidgetId]: {
problem: {
name: {
$set: newName
}
}
}

Thomas Roos
committed
})
}))
}}
saveProblemName={this.saveProblemName}

Thomas Roos
committed
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
/>
<this.PanelExamActions />
</React.Fragment>
)
}
PanelEdit = (props) => {
return (
<nav className='panel'>
<p className='panel-heading'>
Problem details
</p>
<div className='panel-block'>
<div className='field'>
<label className='label'>Name</label>
<div className='control'>
<input
disabled={props.disabled}
className='input'
placeholder='Problem name'
value={props.problem ? props.problem.name : ''}
onChange={(e) => {
console.log('onChange')
props.changeProblemName(e.target.value)
}}
onBlur={(e) => {
console.log('onBlur')
props.saveProblemName(e.target.value)
}} />
</div>

Thomas Roos
committed
</div>
<div className='panel-block'>
<button
disabled={props.disabled}
className='button is-danger is-fullwidth'
onClick={() => props.onDeleteClick()}
>
Delete
</button>
</div>

Thomas Roos
committed
</nav>
)
}
PanelExamActions = () => {
if (this.props.exam.finalized) {
return <PanelGenerate examID={this.state.examID} />
}

Thomas Roos
committed
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
return (
<nav className='panel'>
<p className='panel-heading'>
Actions
</p>
<div className='panel-block'>
{this.state.previewing
? <this.PanelConfirm
onYesClick={() =>
api.put(`exams/${this.props.examID}/finalized`, 'true')
.then(() => {
this.props.updateExam(this.props.examID)
this.setState({ previewing: false })
})
}
onNoClick={() => this.setState({
previewing: false
})}
/>
: <this.Finalize
onFinalizeClicked={() => this.setState({
previewing: true
})}
/>
}
</div>
</nav>
)
}
Finalize = (props) => {
return (
<button
className='button is-link is-fullwidth'
onClick={() => props.onFinalizeClicked()}
>
Finalize
</button>
)
}
PanelConfirm = (props) => {
return (
<nav className='panel'>
<div className='content' dangerouslySetInnerHTML={{__html: ExamFinalizeMarkdown}} />

Thomas Roos
committed
<div className='panel-heading'>
<label className='label'>Are you sure?</label>
</div>
<div className='panel-block'>
<div className='field has-addons'>
<div className='control'>
<button
disabled={props.disabled}
className='button is-danger'
onClick={() => props.onYesClick()}
>
Yes
</button>
</div>
<div className='control'>
<button
disabled={props.disabled}
className='button is-link'
onClick={() => props.onNoClick()}
>
No
</button>

Thomas Roos
committed
</div>
</nav>
)
}

Thomas Roos
committed
render () {
return <div>
<Hero />
<section className='section'>
<div className='container'>
<div className='columns is-centered' >
<div className='column is-one-quarter-widescreen is-one-third-desktop editor-side-panel' >
<p className='title is-1'>Exam details</p>
<p className='subtitle is-3'>{'Selected: ' + this.props.exam.name}</p>
<this.Pager
page={this.state.page}
numPages={this.state.numPages}
setPage={this.setPage}
/>
<this.SidePanel examID={this.state.examID} />
</div>
<div className='column is-narrow editor-content' >
{this.renderContent()}

Thomas Roos
committed
</div>
</section>
</div>
}