Admin.tsx 32 KB

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