A Pen by Ibitoye Best on CodePen.
Created
June 2, 2021 13:29
-
-
Save rotimi-best/88cf56e5b7d083e61f837a64becb0f3b to your computer and use it in GitHub Desktop.
Questionnaire Builder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id="root"></div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { | |
AppBar, | |
CssBaseline, | |
Typography, | |
Toolbar, | |
Card, | |
CardActions, | |
CardContent, | |
Icon, | |
IconButton, | |
Button, | |
makeStyles, | |
TextField, | |
FormControl, | |
InputLabel, | |
Select, | |
MenuItem, | |
Tooltip, | |
Fab, | |
Grid | |
} = MaterialUI; | |
const toolTipUseStyles = makeStyles((theme) => ({ | |
absolute: { | |
position: 'fixed', | |
bottom: theme.spacing(2), | |
right: theme.spacing(3), | |
}, | |
})); | |
function SimpleTooltips({ handleAdd }) { | |
const classes = toolTipUseStyles(); | |
return ( | |
<Tooltip title="Add New Question" aria-label="add" onClick={handleAdd}> | |
<Fab color="secondary" className={classes.absolute}> | |
<Icon>add</Icon> | |
</Fab> | |
</Tooltip> | |
); | |
} | |
const headerUseStyles = makeStyles((theme) => ({ | |
root: { | |
flexGrow: 1, | |
}, | |
title: { | |
flexGrow: 1, | |
}, | |
})); | |
function Header() { | |
const classes = headerUseStyles(); | |
return ( | |
<div className={classes.root}> | |
<AppBar> | |
<Toolbar> | |
<IconButton edge="start" color="default" aria-label="menu"> | |
<Icon>menu</Icon> | |
</IconButton> | |
<Typography variant="h6" className={classes.title}> | |
Questionnaire Builder | |
</Typography> | |
<Button color="inherit">Preview</Button> | |
</Toolbar> | |
</AppBar> | |
<Toolbar /> | |
<Toolbar /> | |
</div> | |
); | |
} | |
const questioHeadernUseStyles = makeStyles({ | |
root: { | |
width: '100%', | |
}, | |
text: { | |
width: '100%' | |
}, | |
}); | |
function QuestionnaireHeader() { | |
const classes = questioHeadernUseStyles(); | |
const [state, setState] = React.useState({ | |
cancerType: '', | |
uniqueId: '' | |
}); | |
const handleChange = (type) => e => { | |
setState({ | |
...state, | |
[type]: e.target.value | |
}) | |
}; | |
return ( | |
<Card className={classes.root}> | |
<CardContent> | |
<TextField | |
id="standard-basic" | |
label="Cancer Type" | |
value={state.cancerType} | |
className={classes.text} | |
onChange={handleChange('cancerType')} | |
/> | |
<TextField | |
id="standard-basic" | |
label="Unique ID" | |
value={state.uniqueId} | |
className={classes.text} | |
onChange={handleChange('uniqueId')} | |
/> | |
</CardContent> | |
</Card> | |
) | |
} | |
const conditionUseStyles = makeStyles(theme => ({ | |
root: { | |
width: '100%', | |
marginTop: 18 | |
}, | |
title: { | |
marginTop: 20 | |
}, | |
formControl: { | |
margin: theme.spacing(1), | |
minWidth: 160, | |
// width: '100%' | |
}, | |
})); | |
const conditionOptions = { | |
questions: ["Question 1", "Question 2", "Question 3"], | |
condition: ["Contains one of", "Equal to", "Doensn't contain"], | |
options: ["Option 1", "Option 2", "Option 3"] | |
} | |
const Condition = props => { | |
const classes = conditionUseStyles(); | |
const [state, setState] = React.useState({ | |
questions: '', | |
condition: '', | |
options: '' | |
}) | |
const handleChange = (key) => (e) => { | |
setState({ | |
...state, | |
[key]: e.target.value | |
}); | |
}; | |
return ( | |
<div className={classes.root}> | |
<Typography component="h6" variant="h6" className={classes.title}> | |
Condition <Fab color="primary" size="small"> | |
<Icon>add</Icon> | |
</Fab> | |
</Typography> | |
<Grid container> | |
<Grid item xs={4}> | |
<FormControl className={classes.formControl}> | |
<InputLabel id="select-question-label">Question</InputLabel> | |
<Select | |
labelId="select-question-label" | |
id="select-question" | |
value={state.questions} | |
onChange={handleChange('questions')} | |
> | |
{conditionOptions.questions.map(q => <MenuItem value={q}>{q}</MenuItem>)} | |
</Select> | |
</FormControl> | |
</Grid> | |
<Grid item xs={4}> | |
<FormControl className={classes.formControl}> | |
<InputLabel id="select-question-label">Condition</InputLabel> | |
<Select | |
labelId="select-condition-label" | |
id="select-condition" | |
value={state.condition} | |
onChange={handleChange('condition')} | |
> | |
{conditionOptions.condition.map(q => <MenuItem value={q}>{q}</MenuItem>)} | |
</Select> | |
</FormControl> | |
</Grid> | |
<Grid item xs={4}> | |
<FormControl className={classes.formControl}> | |
<InputLabel id="select-question-label">Options</InputLabel> | |
<Select | |
labelId="select-options-label" | |
id="select-options" | |
value={state.options} | |
onChange={handleChange('options')} | |
> | |
{conditionOptions.options.map(q => <MenuItem value={q}>{q}</MenuItem>)} | |
</Select> | |
</FormControl> | |
</Grid> | |
</Grid> | |
</div> | |
) | |
} | |
const radioUseStyles = makeStyles(theme => ({ | |
root: { | |
width: '100%', | |
marginTop: 12 | |
}, | |
optionTitle: { | |
marginTop: 20 | |
}, | |
text: { | |
width: '100%', | |
display: 'flex', | |
alignItems: 'center', | |
height: '100%', | |
justifyContent: 'center' | |
}, | |
closeBtn: { | |
display: 'flex', | |
alignItems: 'center', | |
justifyContent: 'center' | |
}, | |
input: { | |
width: '100%' | |
}, | |
})); | |
const Option = ({ classes, option, index, handleChange, handleRemove }) => ( | |
<React.Fragment> | |
<Grid item xs={1}> | |
<Typography component="p" variant="subtitle2" className={classes.text}> | |
{index + 1}. | |
</Typography> | |
</Grid> | |
<Grid item xs={10}> | |
<TextField | |
id="standard-basic" | |
label={`Label ${index + 1}`} | |
value={option.label} | |
// key="label" | |
className={classes.input} | |
onChange={handleChange(index, 'label')} | |
/> | |
<TextField | |
id="standard-basic" | |
label={`Value ${index + 1}`} | |
value={option.value} | |
className={classes.input} | |
onChange={handleChange(index, 'value')} | |
/> | |
{/**/} | |
</Grid> | |
<Grid item xs={1} className={classes.closeBtn}> | |
<IconButton | |
edge="start" | |
color="default" | |
aria-label="menu" | |
onClick={handleRemove(index)} | |
> | |
<Icon>close</Icon> | |
</IconButton> | |
</Grid> | |
</React.Fragment> | |
) | |
function RadioOptions() { | |
const classes = radioUseStyles(); | |
const [options, setOptions] = React.useState([]); | |
const handleOptionChange = (index, key) => (e) => { | |
const updatedOptions = [...options]; | |
updatedOptions[index] = { | |
...updatedOptions[index], | |
[key]: e.target.value | |
}; | |
setOptions(updatedOptions) | |
} | |
const handleFocus = () => { | |
setOptions(options => ([ | |
...options, | |
{ | |
label: '', | |
value: '' | |
} | |
])); | |
} | |
const handleOptionRemove = (index) => () => { | |
setOptions(options => options.filter((option, i) => i !== index)) | |
} | |
const AddOption = () => ( | |
<> | |
<Grid item xs={10}> | |
<TextField | |
label="Add option" | |
onFocus={handleFocus} | |
/> | |
</Grid> | |
</> | |
) | |
return ( | |
<div> | |
<Typography component="h6" variant="h6" className={classes.optionTitle}> | |
Options | |
</Typography> | |
<Grid container justify="center" spacing={1}> | |
{options.map((option, i) => | |
<Option | |
key={`option_${i}`} | |
classes={classes} | |
option={option} | |
index={i} | |
handleChange={handleOptionChange} | |
handleRemove={handleOptionRemove} | |
/> | |
)} | |
<AddOption /> | |
</Grid> | |
</div> | |
) | |
} | |
const questionTemplateUseStyles = makeStyles(theme => ({ | |
root: { | |
width: '100%', | |
marginTop: 18, | |
position: 'relative' | |
}, | |
text: { | |
width: '100%' | |
}, | |
formControl: { | |
marginTop: theme.spacing(1), | |
minWidth: 200, | |
}, | |
button: { | |
position: 'absolute', | |
top: 0, | |
right: 0 | |
} | |
})); | |
function QuestionTemplate(props) { | |
const { | |
handleDelete, | |
index, | |
question | |
} = props; | |
const classes = questionTemplateUseStyles(); | |
const [state, setState] = React.useState({ | |
...question | |
}); | |
const handleChange = (type) => e => { | |
setState({ | |
...state, | |
[type]: e.target.value | |
}) | |
}; | |
return ( | |
<Card className={classes.root}> | |
<IconButton | |
className={classes.button} | |
onClick={() => {handleDelete(question._id)}} | |
> | |
<Icon>close</Icon> | |
</IconButton> | |
<CardContent> | |
<TextField | |
label="Question ID" | |
value={state.qId} | |
className={classes.text} | |
onChange={handleChange('qId')} | |
/> | |
<TextField | |
label="ID" | |
value={state.id} | |
className={classes.text} | |
onChange={handleChange('id')} | |
/> | |
<TextField | |
label="Title" | |
value={state.title} | |
className={classes.text} | |
onChange={handleChange('title')} | |
/> | |
<FormControl variant="outlined" className={classes.formControl}> | |
<InputLabel id="demo-simple-select-outlined-label">Option</InputLabel> | |
<Select | |
labelId="question-type" | |
value={state.type} | |
onChange={handleChange('type')} | |
label="Type" | |
> | |
<MenuItem value="radio">Radio</MenuItem> | |
<MenuItem value="checkbox">Checkbox</MenuItem> | |
<MenuItem value="number">Number</MenuItem> | |
</Select> | |
</FormControl> | |
{["radio", "checkbox"].includes(state.type) && <RadioOptions />} | |
<Condition /> | |
</CardContent> | |
<CardActions> | |
{/* | |
<Button | |
variant="contained" | |
color="primary" | |
className={classes.button} | |
startIcon={<Icon>add</Icon>} | |
> | |
Add parameter | |
</Button> | |
*/} | |
</CardActions> | |
</Card> | |
) | |
} | |
const defaultQuestionProp = (_id = 0) => ({ | |
_id, | |
qId: '', | |
id: '', | |
title: '', | |
subtitle: '', | |
type: '', | |
}) | |
const questionUseStyles = makeStyles({ | |
root: { | |
minWidth: 250, | |
width: 770, | |
margin: 'auto', | |
paddingBottom: 64 | |
} | |
}); | |
function Questionnaire() { | |
const classes = questionUseStyles(); | |
const [questions, setQuestions] = React.useState([defaultQuestionProp()]); | |
const handleDelete = (_id) => { | |
console.log('_id', _id) | |
setQuestions([ | |
...questions.filter((q) => q._id !== _id) | |
]) | |
} | |
const handleAdd = () => { | |
setQuestions([ | |
...questions, | |
defaultQuestionProp(questions.length) | |
]) | |
} | |
return ( | |
<div className={classes.root}> | |
<QuestionnaireHeader /> | |
{questions.map((q) => <QuestionTemplate key={`key-${q._id}`} question={q} handleDelete={handleDelete}/>)} | |
<SimpleTooltips handleAdd={handleAdd} /> | |
</div> | |
) | |
} | |
const App = () => { | |
return ( | |
<div className=""> | |
<CssBaseline /> | |
<Header /> | |
<Questionnaire /> | |
</div> | |
) | |
} | |
ReactDOM.render( | |
<App />, | |
document.getElementById('root') | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://unpkg.com/react/umd/react.development.js"></script> | |
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script> | |
<script src="https://unpkg.com/@material-ui/core@latest/umd/material-ui.development.js"></script> | |
<script src="https://unpkg.com/babel-standalone@latest/babel.min.js"></script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment