Admin.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  1. import React, { useState } from 'react';
  2. import { Container, Badge, FormGroup, Label, Input, Button, Collapse, Table, ListGroup, ListGroupItem } from 'reactstrap';
  3. import Paper, { Path, PaperScope, Point, Raster, Color } from 'paper';
  4. import { without, append, isNil, identity, includes, is } from 'ramda';
  5. import Axios from 'axios';
  6. import VALUES, { AgeVal, GenderVal, NonNativeVal, Filters, FilterVal, FilterStatus, EducationVal } from './services';
  7. import FileDownload from 'js-file-download';
  8. import map from './merseyside-nobg-white.png';
  9. import './Admin.css';
  10. const BACKEND = process.env.REACT_APP_BACKEND || '/backend/';
  11. type PersonalInformation = {
  12. age: AgeVal,
  13. gender: GenderVal,
  14. genderCustom: string,
  15. birthPlace: string,
  16. currentPlace: string,
  17. levelEducation: EducationVal[],
  18. nonNative: NonNativeVal,
  19. }
  20. type Result = {
  21. personalInformation: PersonalInformation,
  22. canvas: OriginalCanvasData[],
  23. canvasSize: {
  24. width: number,
  25. height: number
  26. },
  27. email: string,
  28. id: number,
  29. }
  30. interface StateData {
  31. id: number,
  32. personalInformation: PersonalInformation,
  33. canvas: CanvasData[],
  34. // group: paper.Group,
  35. email: string
  36. };
  37. interface FormPath {
  38. name: string,
  39. soundExample: string,
  40. associations: string[],
  41. correctness: number,
  42. friendliness: number,
  43. pleasantness: number,
  44. trustworthiness: number
  45. }
  46. interface ShapeData extends FormPath, PersonalInformation {
  47. id: number
  48. }
  49. type OriginalCanvasData = {
  50. form: FormPath,
  51. path: string,
  52. }
  53. type CanvasData = {
  54. form: FormPath,
  55. path: paper.Path,
  56. label: paper.PointText
  57. };
  58. type AdminState = {
  59. original: StateData[],
  60. data: StateData[],
  61. canvas?: paper.PaperScope,
  62. focusedResponse?: StateData,
  63. path?: paper.Path,
  64. selectedArea?: paper.Item[]
  65. };
  66. const mkPathLabel = (pathName: string, path: paper.Path) => {
  67. return new Paper.PointText({
  68. fillColor: '#4b505a',
  69. justification: 'center',
  70. fontWeight: 'bold',
  71. fontSize: '16px',
  72. point: path.bounds.center,
  73. content: pathName,
  74. visible: false
  75. })
  76. }
  77. const COLORS = [
  78. '#ffc6bc',
  79. '#fedea2',
  80. '#fff9b8',
  81. '#d3dbb2',
  82. '#a7d3d2',
  83. '#efe3f3',
  84. '#c4d0f5',
  85. '#c0ba98',
  86. ];
  87. const mkBgColor = (color: string) => {
  88. const _color = new Paper.Color(color);
  89. _color.alpha = 0.2;
  90. return _color;
  91. };
  92. const mkPath = (pathData: string, i: number, scale: number, data: ShapeData): paper.Path => {
  93. const color = COLORS[i % COLORS.length];
  94. const _path = new Path();
  95. _path.importJSON(pathData);
  96. _path.strokeColor = new Color(color);
  97. _path.strokeWidth = 2;
  98. _path.fillColor = mkBgColor(color)
  99. _path.selected = false;
  100. _path.scale(scale, new Point(0, 0));
  101. _path.data = data;
  102. return _path;
  103. }
  104. class Admin extends React.Component<{}, AdminState> {
  105. constructor(props: any) {
  106. super(props);
  107. this.state = {
  108. canvas: undefined,
  109. original: [],
  110. data: [],
  111. focusedResponse: undefined,
  112. path: undefined,
  113. selectedArea: undefined
  114. };
  115. }
  116. createPath = (canvas: paper.PaperScope) => (event: any) => {
  117. if(this.state.path === undefined) {
  118. this.toggleDrawings(this.state.data, false);
  119. let path = new canvas.Path({
  120. strokeColor: new Color('red'),
  121. strokeWidth: 5,
  122. });
  123. path.bringToFront();
  124. path.add(event.point);
  125. this.setState({ path });
  126. }
  127. }
  128. addPoint = (event: any) => {
  129. if (this.state.path && this.state.selectedArea === undefined) {
  130. this.state.path?.add(event.point)
  131. }
  132. };
  133. getItems = (canvas: paper.PaperScope) => () => {
  134. const { path, selectedArea } = this.state;
  135. if (path && !selectedArea) {
  136. path.add(path.firstSegment);
  137. path.closePath();
  138. path.simplify();
  139. const items = canvas.project.activeLayer.getItems({
  140. match: (value: paper.Item) => {
  141. return value.data.id && path.intersects(value);
  142. },
  143. class: Path,
  144. });
  145. items.forEach(item => {
  146. item.visible = true
  147. });
  148. this.setState({
  149. selectedArea: items || []
  150. })
  151. }
  152. }
  153. toggleDrawings = (data: StateData[], visibility: boolean) => {
  154. data.forEach(result => {
  155. result.canvas.forEach(({ path }) => {
  156. path.visible = visibility;
  157. })
  158. })
  159. }
  160. mkDrawingTool = (canvas: paper.PaperScope) => {
  161. let Tool = new canvas.Tool();
  162. Tool.onMouseDown = this.createPath(canvas);
  163. Tool.onMouseDrag = this.addPoint;
  164. Tool.onMouseUp = this.getItems(canvas);
  165. }
  166. componentDidMount = () => {
  167. const canvas = new PaperScope();
  168. canvas.setup('vom-admin-canvas');
  169. canvas.view.viewSize.height = canvas.view.size.width * 1.25;
  170. this.mkDrawingTool(canvas);
  171. Axios.get<Result[]>(BACKEND, {
  172. headers: {
  173. 'X-Token': 'secret-potato',
  174. }
  175. }).then(response => {
  176. const _data = response.data.map((result, index) => {
  177. const item = {
  178. ...result,
  179. canvas: result.canvas.map((item, i) => {
  180. const pathData: ShapeData = {
  181. id: result.id,
  182. ...result.personalInformation,
  183. ...item.form
  184. };
  185. const _path = mkPath(item.path, index, canvas.view.size.width / result.canvasSize.width, pathData);
  186. const _text = mkPathLabel(`${item.form.name} (${i})`, _path);
  187. return {
  188. path: _path,
  189. label: _text,
  190. form: item.form
  191. }
  192. }),
  193. };
  194. return item;
  195. })
  196. const raster = new Raster(map);
  197. raster.onLoad = () => {
  198. raster.position = canvas.view.center;
  199. raster.size = canvas.view.viewSize;
  200. raster.bringToFront();
  201. }
  202. this.setState({
  203. canvas,
  204. original: _data,
  205. data: _data,
  206. })
  207. })
  208. }
  209. isNotEducational = (value: AgeVal | GenderVal | NonNativeVal | string | EducationVal[]): value is AgeVal | GenderVal | NonNativeVal | string => {
  210. return is(String, value);
  211. }
  212. applyFilters = (filters: Filters) => {
  213. const results = this.state.original.filter(
  214. result => {
  215. const matches = VALUES.FILTER_KEYS.map(filterKey => {
  216. const filterValues = filters[filterKey];
  217. const resultValue = result.personalInformation[filterKey];
  218. if (filterValues) {
  219. if (this.isNotEducational(resultValue)) {
  220. return !isNil(filterValues.find(v => {
  221. return v === resultValue
  222. }));
  223. } else {
  224. return filterValues.some(v => includes(v, resultValue))
  225. }
  226. } else {
  227. return true;
  228. }
  229. }).every(identity);
  230. if (matches) {
  231. result.canvas.forEach(({ path}) => {
  232. path.visible = true;
  233. })
  234. return true
  235. } else {
  236. result.canvas.forEach(({path}) => {
  237. path.visible = false;
  238. })
  239. return false;
  240. }
  241. }
  242. )
  243. this.setState({
  244. data: results,
  245. focusedResponse: undefined,
  246. })
  247. }
  248. focusPath = (id: number) => {
  249. this.state.data.forEach(result => {
  250. if (result.id === id) {
  251. result.canvas.forEach(({path}) => {
  252. path.visible = true;
  253. })
  254. this.setState({
  255. focusedResponse: result
  256. })
  257. } else {
  258. result.canvas.forEach(({ path}) => {
  259. path.visible = false;
  260. })
  261. }
  262. });
  263. }
  264. clearFocus = () => {
  265. this.state.data.forEach(result => {
  266. result.canvas.forEach(({path}) => {
  267. path.visible = true;
  268. })
  269. })
  270. this.setState({
  271. focusedResponse: undefined
  272. })
  273. }
  274. clearDrawing = () => {
  275. this.state.data.forEach(result => {
  276. result.canvas.forEach(({path}) => {
  277. path.visible = true;
  278. })
  279. });
  280. this.state.path?.remove();
  281. this.setState({
  282. path: undefined,
  283. selectedArea: undefined,
  284. })
  285. }
  286. downloadData = () => {
  287. Axios.get(`${BACKEND}/csv`, {
  288. headers: {
  289. 'X-Token': 'secret-potato',
  290. }
  291. }).then(response => {
  292. FileDownload(response.data, 'voices-of-merseyside.csv')
  293. })
  294. }
  295. render() {
  296. return (
  297. <div className="App Admin">
  298. <Container fluid>
  299. <h2>Administration panel</h2>
  300. <div className="vom-results-data">
  301. <p>total responses: <Badge color="info">{this.state.original.length}</Badge></p>
  302. <p>showing: <Badge color="info">{this.state.data.length}</Badge></p>
  303. <p><Badge color="success" href="#" onClick={() => this.downloadData() }>download</Badge></p>
  304. </div>
  305. <div>
  306. <FilterPanel
  307. applyFilters={this.applyFilters}
  308. ></FilterPanel>
  309. </div>
  310. <div className="vom-canvii">
  311. <canvas id="vom-admin-canvas"></canvas>
  312. <div className="vom-selected-data">
  313. {
  314. this.state.selectedArea ? (
  315. <>
  316. <div>
  317. <p>drawings in area: <Badge color="info">{this.state.selectedArea.length}</Badge> <Badge color="warning" href="#" onClick={() => this.clearDrawing() }>clear drawing</Badge></p>
  318. </div>
  319. <SelectedAreaTable items={this.state.selectedArea}></SelectedAreaTable>
  320. </>
  321. ) : null
  322. }
  323. </div>
  324. {
  325. this.state.focusedResponse ? (
  326. <div id="vom-results-panel">
  327. <div id="vom-focused-result" className="mt-3">
  328. <h4>Showing:</h4>
  329. <ViewResponse response={this.state.focusedResponse}/>
  330. </div>
  331. </div>
  332. ) : null
  333. }
  334. </div>
  335. <div id="vom-results">
  336. <div id="vom-results-table">
  337. <h4>Results</h4>
  338. {
  339. this.state.selectedArea ? (
  340. <DrawingsTable
  341. items={this.state.selectedArea}
  342. />
  343. ) : (
  344. <ResultsTable
  345. data={this.state.data}
  346. focusPath={this.focusPath}
  347. clearFocus={this.clearFocus}
  348. />
  349. )
  350. }
  351. </div>
  352. </div>
  353. </Container>
  354. </div>
  355. )
  356. }
  357. }
  358. type SelectedAreaTotals = {
  359. age: {
  360. [k: string]: number,
  361. '1': number,
  362. '2': number,
  363. '3': number,
  364. '4': number,
  365. '5': number,
  366. '6': number
  367. },
  368. gender: {
  369. [k: string]: number,
  370. 'female': number,
  371. 'male': number,
  372. 'other': number
  373. },
  374. levelEducation: {
  375. [k: string]: number,
  376. '1': number,
  377. '2': number,
  378. '3': number,
  379. '4': number
  380. }
  381. }
  382. const SelectedAreaTable: React.FunctionComponent<{items: paper.Item[]}> = ({ items }) => {
  383. const init = {
  384. age: {
  385. '1': 0,
  386. '2': 0,
  387. '3': 0,
  388. '4': 0,
  389. '5': 0,
  390. '6': 0
  391. },
  392. gender: {
  393. 'female': 0,
  394. 'male': 0,
  395. 'other': 0
  396. },
  397. levelEducation: {
  398. '1': 0,
  399. '2': 0,
  400. '3': 0,
  401. '4': 0
  402. }
  403. };
  404. const [totals] = useState<SelectedAreaTotals>(items.reduce((totals, item) => {
  405. const data = item.data as ShapeData;
  406. const lvlEd = data.levelEducation ? data.levelEducation.reduce((tot, lvl) => {
  407. return {
  408. ...tot,
  409. [lvl]: totals.levelEducation[lvl] + 1
  410. }
  411. }, {}) : {};
  412. return data.age ? {
  413. age: {
  414. ...totals.age,
  415. [data.age]: totals.age[data.age] + 1
  416. },
  417. gender: {
  418. ...totals.gender,
  419. [data.gender]: totals.gender[data.gender] + 1
  420. },
  421. levelEducation: {
  422. ...totals.levelEducation,
  423. ...lvlEd
  424. }
  425. } : totals;
  426. }, { ...init }) || init)
  427. return (
  428. <>
  429. <h6>Age</h6>
  430. <Table bordered>
  431. <thead>
  432. <tr>
  433. <th>16-17</th>
  434. <th>18-25</th>
  435. <th>26-45</th>
  436. <th>46-65</th>
  437. <th>66-75</th>
  438. <th>75+</th>
  439. </tr>
  440. </thead>
  441. <tbody>
  442. <tr>
  443. { Object.keys(totals.age).map((age, key) => (
  444. <td key={key}>{totals.age[age]}</td>
  445. ))}
  446. </tr>
  447. </tbody>
  448. </Table>
  449. <h6>Gender</h6>
  450. <Table bordered>
  451. <thead>
  452. <tr>
  453. <th>female</th>
  454. <th>male</th>
  455. <th>other</th>
  456. </tr>
  457. </thead>
  458. <tbody>
  459. <tr>
  460. {
  461. Object.keys(totals.gender).map((gender, key) => (
  462. <td key={key}>{totals.gender[gender]}</td>
  463. ))
  464. }
  465. </tr>
  466. </tbody>
  467. </Table>
  468. <h6>Level of Education</h6>
  469. <Table bordered>
  470. <thead>
  471. <tr>
  472. <th>high school or lower</th>
  473. <th>bachelors</th>
  474. <th>masters</th>
  475. <th>doctorate</th>
  476. </tr>
  477. </thead>
  478. <tbody>
  479. <tr>
  480. {
  481. Object.keys(totals.levelEducation).map((levelEducation, key) => (
  482. <td key={key}>{totals.levelEducation[levelEducation]}</td>
  483. ))
  484. }
  485. </tr>
  486. </tbody>
  487. </Table>
  488. </>
  489. )
  490. }
  491. type TableProps = {
  492. data: StateData[],
  493. focusPath: (id: number) => void,
  494. clearFocus: () => void,
  495. };
  496. const DrawingsTable: React.FunctionComponent<{items: paper.Item[]}> = ({ items }) => {
  497. return (
  498. <Table bordered >
  499. <thead>
  500. <tr>
  501. <th>#</th>
  502. <th>age</th>
  503. <th>G</th>
  504. <th>education</th>
  505. <th>birth place</th>
  506. <th>current place</th>
  507. <th>NN</th>
  508. <th>name</th>
  509. <th>example</th>
  510. <th>associations</th>
  511. <th>C</th>
  512. <th>F</th>
  513. <th>P</th>
  514. <th>T</th>
  515. </tr>
  516. </thead>
  517. <tbody>
  518. {
  519. items.map(({ data }, key) => {
  520. const d = data as ShapeData;
  521. return d.gender ? (
  522. <tr key={key}>
  523. <td>{d.id}</td>
  524. <td>{VALUES.AGE[d.age] || ''}</td>
  525. <td>{d.gender[0] || ''}</td>
  526. <td>{d.levelEducation.map(e => VALUES.EDUCATION[e] || '').join(', ')}</td>
  527. <td>{d.birthPlace}</td>
  528. <td>{d.currentPlace}</td>
  529. <td>{VALUES.NON_NATIVE[d.nonNative] || '-'}</td>
  530. <td>{d.name}</td>
  531. <td>{d.soundExample || '-'}</td>
  532. <td>{d.associations.join(', ')}</td>
  533. <td>{d.correctness}</td>
  534. <td>{d.friendliness}</td>
  535. <td>{d.pleasantness}</td>
  536. <td>{d.trustworthiness}</td>
  537. </tr>
  538. ) : null
  539. })
  540. }
  541. </tbody>
  542. </Table>
  543. )
  544. }
  545. const ResultsTable: React.FunctionComponent<TableProps> = ({ data, focusPath, clearFocus }) => {
  546. const [ focused, setFocused ] = useState<number>();
  547. return (
  548. <>
  549. <div className="vom-results-table__actions">
  550. <Button onClick={() => {
  551. setFocused(undefined);
  552. clearFocus();
  553. }} outline>clear focus</Button>
  554. </div>
  555. <Table bordered className="vom-table-results">
  556. <thead>
  557. <tr>
  558. <th>#</th>
  559. <th>age</th>
  560. <th>G</th>
  561. <th>education</th>
  562. <th>birth place</th>
  563. <th>current place</th>
  564. <th>NN</th>
  565. <th>#</th>
  566. <th>name</th>
  567. <th>example</th>
  568. <th>associations</th>
  569. <th>C</th>
  570. <th>F</th>
  571. <th>P</th>
  572. <th>T</th>
  573. </tr>
  574. </thead>
  575. <tbody>
  576. {
  577. data.map(({ id, personalInformation, canvas }) => {
  578. return canvas.map(({ form }, i) => (
  579. <tr
  580. key={i}
  581. onClick={() => { focusPath(id); setFocused(id) }}
  582. className={
  583. `${i === 0 ? 'main-row' : ''}
  584. ${id === focused ? 'data-focused' : ''}
  585. `
  586. }
  587. >
  588. {
  589. i === 0 ? (
  590. <>
  591. <th rowSpan={canvas.length} scope="rowGroup">{id}</th>
  592. <th rowSpan={canvas.length} scope="rowGroup">{VALUES.AGE[personalInformation.age]}</th>
  593. <th rowSpan={canvas.length} scope="rowGroup">{personalInformation.gender[0]}</th>
  594. <th rowSpan={canvas.length} scope="rowGroup">{personalInformation.levelEducation.map(e => VALUES.EDUCATION[e])}</th>
  595. <th rowSpan={canvas.length} scope="rowGroup">{personalInformation.birthPlace}</th>
  596. <th rowSpan={canvas.length} scope="rowGroup">{personalInformation.currentPlace}</th>
  597. <th rowSpan={canvas.length} scope="rowGroup">{VALUES.NON_NATIVE[personalInformation.nonNative] || '-'}</th>
  598. </>
  599. ) : null
  600. }
  601. <td>{i}</td>
  602. <td>{form.name}</td>
  603. <td>{form.soundExample || '-'}</td>
  604. <td>{form.associations.join(', ')}</td>
  605. <td>{form.correctness}</td>
  606. <td>{form.friendliness}</td>
  607. <td>{form.pleasantness}</td>
  608. <td>{form.trustworthiness}</td>
  609. </tr>
  610. ))
  611. })
  612. }
  613. </tbody>
  614. </Table>
  615. </>
  616. )
  617. }
  618. const FilterPanel: React.FunctionComponent<{
  619. applyFilters: (filters: Filters) => void
  620. }> = ({ applyFilters }) => {
  621. const [filters, setFilters] = useState<Filters>({
  622. ...VALUES.FILTER,
  623. nonNative: undefined
  624. });
  625. const [activeFilters, setActiveFilters] = useState<FilterStatus>({
  626. age: true,
  627. gender: true,
  628. levelEducation: true,
  629. nonNative: false,
  630. });
  631. const isFilterActive = (field: FilterVal) => {
  632. return activeFilters[field]
  633. };
  634. const activateFilter = (field: FilterVal) => ({ target }: React.ChangeEvent<HTMLInputElement>) => {
  635. const _filters = {
  636. ...activeFilters,
  637. [field]: target.checked
  638. }
  639. if (target.checked) {
  640. handleFilter({
  641. ...filters,
  642. [field]: VALUES.FILTER[field]
  643. });
  644. } else {
  645. handleFilter({
  646. ...filters,
  647. [field]: undefined
  648. })
  649. }
  650. setActiveFilters(_filters);
  651. }
  652. const selectAll = () => {
  653. handleFilter({
  654. ...VALUES.FILTER,
  655. nonNative: undefined
  656. })
  657. }
  658. const isChecked = (field: FilterVal, value: string) => {
  659. const values = filters[field];
  660. if (values){
  661. return includes(value, values);
  662. } else {
  663. return false
  664. }
  665. }
  666. const handleCheck = (field: FilterVal, value: string) => ({ target }: React.ChangeEvent<HTMLInputElement>) => {
  667. const _filters = {
  668. ...filters,
  669. [field]: target.checked ? append(value, filters[field] || []) : without([ value ], filters[field] || [])
  670. };
  671. handleFilter(_filters)
  672. }
  673. const handleFilter = (_filters: Filters) => {
  674. applyFilters(_filters);
  675. setFilters(_filters)
  676. }
  677. return (
  678. <div id="vom-results-filters">
  679. <h4>
  680. Filters
  681. <div id="vom-filter-actions">
  682. <Button outline size="sm" color="secondary" onClick={selectAll}>Select all</Button>
  683. <Button outline size="sm" color="secondary" onClick={() => handleFilter({ ...VALUES.CLEAN_FILTER })} disabled>Clear all</Button>
  684. </div>
  685. </h4>
  686. <div className="vom-filter-switch-group mb-3">
  687. <div className="vom-filter-switch">
  688. <Switch id="age-switch" checked={isFilterActive('age')} onChange={activateFilter('age')}>
  689. <h6>
  690. Age
  691. </h6>
  692. </Switch>
  693. <Collapse isOpen={isFilterActive('age')}>
  694. <FormGroup className="vom-filter-group">
  695. {
  696. (Object.keys(VALUES.AGE) as AgeVal[]).map(value => (
  697. <FormGroup check inline key={value}>
  698. <Label check>
  699. <Input type="checkbox" checked={isChecked('age', value)} onChange={handleCheck('age', value)}/>{VALUES.AGE[value]}
  700. </Label>
  701. </FormGroup>
  702. ))
  703. }
  704. </FormGroup>
  705. </Collapse>
  706. </div>
  707. <div className="vom-filter-switch">
  708. <Switch id="gender-switch" checked={isFilterActive('gender')} onChange={activateFilter('gender')}>
  709. <h6>Gender</h6>
  710. </Switch>
  711. <Collapse isOpen={isFilterActive('gender')}>
  712. <FormGroup className="vom-filter-group">
  713. {
  714. VALUES.GENDER.map(value => (
  715. <FormGroup check inline key={value}>
  716. <Label check>
  717. <Input disabled={!isFilterActive('gender')} type="checkbox" checked={isChecked('gender', value)} onChange={handleCheck('gender', value)}/>{value}
  718. </Label>
  719. </FormGroup>
  720. ))
  721. }
  722. </FormGroup>
  723. </Collapse>
  724. </div>
  725. <div className="vom-filter-switch">
  726. <Switch id="education-switch" checked={isFilterActive('levelEducation')} onChange={activateFilter('levelEducation')}>
  727. <h6>Level of education</h6>
  728. </Switch>
  729. <Collapse isOpen={isFilterActive('levelEducation')}>
  730. <FormGroup className="vom-filter-group">
  731. {
  732. (Object.keys(VALUES.EDUCATION) as EducationVal[]).map(value => (
  733. <FormGroup check inline key={value}>
  734. <Label check>
  735. <Input disabled={!isFilterActive('levelEducation')} type="checkbox" checked={isChecked('levelEducation', value)} onChange={handleCheck('levelEducation', value)}/>
  736. {VALUES.EDUCATION[value]}
  737. </Label>
  738. </FormGroup>
  739. ))
  740. }
  741. </FormGroup>
  742. </Collapse>
  743. </div>
  744. <div className="vom-filter-switch">
  745. <Switch id="non-native-switch" checked={isFilterActive('nonNative')} onChange={activateFilter('nonNative')}>
  746. <h6> Non natives </h6>
  747. </Switch>
  748. <Collapse isOpen={isFilterActive('nonNative')}>
  749. <FormGroup className="vom-filter-group">
  750. {
  751. (Object.keys(VALUES.NON_NATIVE) as NonNativeVal[]).map(value => (
  752. <FormGroup check inline key={value}>
  753. <Label check>
  754. <Input disabled={!isFilterActive('nonNative')} type="checkbox" checked={isChecked('nonNative', value)} onChange={handleCheck('nonNative', value)}/>{VALUES.NON_NATIVE[value]}
  755. </Label>
  756. </FormGroup>
  757. ))
  758. }
  759. </FormGroup>
  760. </Collapse>
  761. </div>
  762. </div>
  763. </div>
  764. )
  765. }
  766. const Switch: React.FunctionComponent<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>> = ({ id, children , ...props}) => {
  767. return (
  768. <div className="custom-control custom-switch">
  769. <input type="checkbox" className="custom-control-input" id={id} {...props} />
  770. <label className="custom-control-label" htmlFor={id}>
  771. { children }
  772. </label>
  773. </div>
  774. )
  775. };
  776. const ViewResponse: React.FunctionComponent<{response: StateData}> = ({ response }) => {
  777. const {
  778. personalInformation,
  779. email,
  780. canvas
  781. } = response;
  782. return (
  783. <div className="vom-view-response">
  784. <ListGroup>
  785. <ListGroupItem>
  786. <b>Age</b>: {VALUES.AGE[personalInformation.age]}
  787. </ListGroupItem>
  788. <ListGroupItem>
  789. <b>Gender</b>: {personalInformation.gender}
  790. </ListGroupItem>
  791. <ListGroupItem>
  792. <b>Education</b>: {personalInformation.levelEducation.map(e => VALUES.EDUCATION[e]).join(', ')}
  793. </ListGroupItem>
  794. <ListGroupItem>
  795. <b>Birth Place</b>: {personalInformation.birthPlace}
  796. </ListGroupItem>
  797. <ListGroupItem>
  798. <b>Current Place</b>: {personalInformation.currentPlace}
  799. </ListGroupItem>
  800. <ListGroupItem>
  801. <b>Non Native?</b>: {VALUES.NON_NATIVE[personalInformation.nonNative] || '-'}
  802. </ListGroupItem>
  803. <ListGroupItem>
  804. <b>email</b>: {email || '-'}
  805. </ListGroupItem>
  806. {
  807. canvas.map(({form}, i) => (
  808. <ListGroupItem key={i}>
  809. <b>Accent name</b>: {form.name} ({i})<br/>
  810. <b>Example</b>: {form.soundExample} <br/>
  811. <b>Associations</b>: {form.associations.join(', ')} <br/>
  812. <b>Correctness</b>: {form.correctness} <br/>
  813. <b>Friendliness</b>: {form.friendliness} <br/>
  814. <b>Pleasantness</b>: {form.pleasantness} <br/>
  815. <b>Trustworthiness</b>: {form.trustworthiness} <br/>
  816. </ListGroupItem>
  817. ))
  818. }
  819. </ListGroup>
  820. </div>
  821. )
  822. }
  823. export default Admin;