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)
}

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
})
}))
}}
saveProblemName={newName => {
api.put('problems/' + problem.id + '/name', { name: newName })
.catch(e => alert('Could not save new problem name: ' + e))
}}
/>
<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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
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
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>
}