Admin.tsx 28 KB

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