Admin.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import React, { useState } from 'react';
  2. import { Container, Badge, FormGroup, Label, Input, Button, Collapse } from 'reactstrap';
  3. import Paper, { Path, PaperScope, Group, Color } from 'paper';
  4. import { equals, without, append, isNil, identity, isEmpty } from 'ramda';
  5. import Axios from 'axios';
  6. import VALUES, { AgeVal, GenderVal, EthnicityVal, NonNativeVal, Filters, FilterVal, FilterStatus } from './services';
  7. type Result = {
  8. personalInformation: {
  9. age: AgeVal,
  10. gender: GenderVal,
  11. genderCustom: string,
  12. ethnicity: EthnicityVal,
  13. ethnicityCustom: string,
  14. birthPlace: string,
  15. currentPlace: string,
  16. nonNative: NonNativeVal
  17. },
  18. canvas: OriginalCanvasData[],
  19. email: string
  20. }
  21. type PersonalInformation = Record<FilterVal, string> & {
  22. age: AgeVal,
  23. gender: GenderVal,
  24. genderCustom: string,
  25. ethnicity: EthnicityVal,
  26. ethnicityCustom: string,
  27. birthPlace: string,
  28. currentPlace: string,
  29. nonNative: NonNativeVal
  30. }
  31. type StateData = {
  32. personalInformation: PersonalInformation,
  33. canvas?: CanvasData[],
  34. group: paper.Group,
  35. email: string
  36. };
  37. type FormPath = {
  38. name: 'string',
  39. soundExample: 'string',
  40. associations: 'string'
  41. }
  42. type OriginalCanvasData = {
  43. form: FormPath,
  44. path: string,
  45. }
  46. type CanvasData = {
  47. form: FormPath,
  48. path: paper.Path
  49. };
  50. type AdminState = {
  51. original: StateData[],
  52. data: StateData[],
  53. canvas?: paper.PaperScope
  54. };
  55. const mkPathLabel = (pathName: string, path: paper.Path) => {
  56. return new Paper.PointText({
  57. fillColor: '#4b505a',
  58. justification: 'center',
  59. fontWeight: 'bold',
  60. fontSize: '16px',
  61. point: path.bounds.center,
  62. content: pathName
  63. })
  64. }
  65. const COLORS = [
  66. '#ffc6bc',
  67. '#fedea2',
  68. '#fff9b8',
  69. '#d3dbb2',
  70. '#a7d3d2',
  71. '#efe3f3',
  72. '#c4d0f5',
  73. '#c0ba98',
  74. ];
  75. const mkBgColor = (color: string) => {
  76. const _color = new Paper.Color(color);
  77. _color.alpha = 0.5;
  78. return _color;
  79. };
  80. const mkPath = (pathData: string, i: number): paper.Path => {
  81. const color = COLORS[i % COLORS.length];
  82. const _path = new Path();
  83. _path.importJSON(pathData);
  84. _path.strokeColor = new Color(color);
  85. _path.strokeWidth = 5;
  86. _path.fillColor = mkBgColor(color)
  87. return _path;
  88. }
  89. class Admin extends React.Component<{}, AdminState> {
  90. constructor(props: any) {
  91. super(props);
  92. this.state = {
  93. canvas: undefined,
  94. original: [],
  95. data: [],
  96. };
  97. }
  98. mkPathGroup = (data: OriginalCanvasData[], index: number) => {
  99. const _data = data.reduce<(paper.Path|paper.Item)[]>((group, value) => {
  100. const _path = mkPath(value.path, index);
  101. const _text = mkPathLabel(value.form.name, _path);
  102. return [
  103. ...group,
  104. _path,
  105. _text,
  106. ]
  107. }, []);
  108. const group = new Group(_data);
  109. return group;
  110. }
  111. componentDidMount = () => {
  112. const canvas = new PaperScope();
  113. canvas.setup('vom-admin-canvas');
  114. Axios.get<Result[]>('https://voicesofmerseyside.inama.dev/backend/').then(response => {
  115. const _data = response.data.map((result, index) => {
  116. return {
  117. ...result,
  118. canvas: undefined,
  119. group: this.mkPathGroup(result.canvas, index)
  120. };
  121. })
  122. this.setState({
  123. canvas,
  124. original: _data,
  125. data: _data,
  126. })
  127. })
  128. }
  129. applyFilters = (filters: Filters) => {
  130. const results = this.state.original.filter(
  131. result => {
  132. const matches = VALUES.FILTER_KEYS.map(filterKey => {
  133. const appliedFilter = filters[filterKey];
  134. if (appliedFilter) {
  135. return !isNil(filters[filterKey].find(equals<string>(result.personalInformation[filterKey])))
  136. } else {
  137. return true;
  138. }
  139. }).every(identity);
  140. if (matches) {
  141. result.group.visible = true;
  142. return true
  143. } else {
  144. result.group.visible = false;
  145. return false;
  146. }
  147. }
  148. )
  149. this.setState({
  150. data: results
  151. })
  152. }
  153. render() {
  154. return (
  155. <div className="App Admin">
  156. <Container fluid>
  157. <h2>Administration panel</h2>
  158. <div className="vom-results-data">
  159. <p>total responses: <Badge color="info">{this.state.original.length}</Badge></p>
  160. <p>showing: <Badge color="info">{this.state.data.length}</Badge></p>
  161. </div>
  162. <div id="vom-results">
  163. <canvas id="vom-admin-canvas"></canvas>
  164. <FilterPanel
  165. applyFilters={this.applyFilters}
  166. ></FilterPanel>
  167. </div>
  168. </Container>
  169. </div>
  170. )
  171. }
  172. }
  173. const FilterPanel: React.FunctionComponent<{
  174. applyFilters: (filters: Filters) => void
  175. }> = ({ applyFilters }) => {
  176. const [filters, setFilters] = useState<Filters>({ ...VALUES.FILTER });
  177. const [activeFilters, setActiveFilters] = useState<FilterStatus>({
  178. age: true,
  179. ethnicity: true,
  180. gender: true,
  181. nonNative: false
  182. });
  183. const isFilterActive = (field: FilterVal) => {
  184. return activeFilters[field]
  185. };
  186. const activateFilter = (field: FilterVal) => ({ target }: React.ChangeEvent<HTMLInputElement>) => {
  187. const _filters = {
  188. ...activeFilters,
  189. [field]: target.checked
  190. }
  191. setActiveFilters(_filters);
  192. if (target.checked) {
  193. applyFilters(filters)
  194. } else {
  195. applyFilters({
  196. ...filters,
  197. [field]: undefined
  198. })
  199. }
  200. }
  201. const isChecked = (field: FilterVal, value: string) => {
  202. return !!filters[field].find(equals(value));
  203. }
  204. const handleCheck = (field: FilterVal, value: string) => ({ target }: React.ChangeEvent<HTMLInputElement>) => {
  205. const _filters = {
  206. ...filters,
  207. [field]: target.checked ? append(value, filters[field]) : without([ value ], filters[field])
  208. };
  209. handleFilter(_filters)
  210. }
  211. const handleFilter = (filters: Filters) => {
  212. applyFilters(filters);
  213. setFilters(filters)
  214. }
  215. return (
  216. <div id="vom-results-filters">
  217. <h4>
  218. Filters
  219. <div id="vom-filter-actions">
  220. <Button outline size="sm" color="secondary" onClick={() => handleFilter({ ...VALUES.FILTER })}>Select all</Button>
  221. <Button outline size="sm" color="secondary" onClick={() => handleFilter({ ...VALUES.CLEAN_FILTER })} disabled>Clear all</Button>
  222. </div>
  223. </h4>
  224. <Switch id="age-switch" checked={isFilterActive('age')} onChange={activateFilter('age')}>
  225. <h6>
  226. Age
  227. </h6>
  228. </Switch>
  229. <Collapse isOpen={isFilterActive('age')}>
  230. <FormGroup className="vom-filter-group">
  231. {
  232. (Object.keys(VALUES.AGE) as AgeVal[]).map(value => (
  233. <FormGroup check inline key={value}>
  234. <Label check>
  235. <Input type="checkbox" checked={isChecked('age', value)} onChange={handleCheck('age', value)}/>{VALUES.AGE[value]}
  236. </Label>
  237. </FormGroup>
  238. ))
  239. }
  240. </FormGroup>
  241. </Collapse>
  242. <hr></hr>
  243. <Switch id="gender-switch" checked={isFilterActive('gender')} onChange={activateFilter('gender')}>
  244. <h6>Gender</h6>
  245. </Switch>
  246. <Collapse isOpen={isFilterActive('gender')}>
  247. <FormGroup className="vom-filter-group">
  248. {
  249. VALUES.GENDER.map(value => (
  250. <FormGroup check inline key={value}>
  251. <Label check>
  252. <Input disabled={!isFilterActive('gender')} type="checkbox" checked={isChecked('gender', value)} onChange={handleCheck('gender', value)}/>{value}
  253. </Label>
  254. </FormGroup>
  255. ))
  256. }
  257. </FormGroup>
  258. </Collapse>
  259. <hr/>
  260. <Switch id="ethnicity-switch" checked={isFilterActive('ethnicity')} onChange={activateFilter('ethnicity')}>
  261. <h6>ethnicity</h6>
  262. </Switch>
  263. <Collapse isOpen={isFilterActive('ethnicity')}>
  264. <FormGroup className="vom-filter-group">
  265. {
  266. VALUES.ETHNICITY.map(value => (
  267. <FormGroup check inline key={value}>
  268. <Label check>
  269. <Input disabled={!isFilterActive('ethnicity')} type="checkbox" checked={isChecked('ethnicity', value)} onChange={handleCheck('ethnicity', value)}/>{value}
  270. </Label>
  271. </FormGroup>
  272. ))
  273. }
  274. </FormGroup>
  275. </Collapse>
  276. <hr/>
  277. <Switch id="non-native-switch" checked={isFilterActive('nonNative')} onChange={activateFilter('nonNative')}>
  278. <h6> Non natives </h6>
  279. </Switch>
  280. <Collapse isOpen={isFilterActive('nonNative')}>
  281. <FormGroup className="vom-filter-group">
  282. {
  283. (Object.keys(VALUES.NON_NATIVE) as NonNativeVal[]).map(value => (
  284. <FormGroup check inline key={value}>
  285. <Label check>
  286. <Input disabled={!isFilterActive('nonNative')} type="checkbox" checked={isChecked('nonNative', value)} onChange={handleCheck('nonNative', value)}/>{VALUES.NON_NATIVE[value]}
  287. </Label>
  288. </FormGroup>
  289. ))
  290. }
  291. </FormGroup>
  292. </Collapse>
  293. </div>
  294. )
  295. }
  296. const Switch: React.FunctionComponent<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>> = ({ id, children , ...props}) => {
  297. return (
  298. <div className="custom-control custom-switch">
  299. <input type="checkbox" className="custom-control-input" id={id} {...props} />
  300. <label className="custom-control-label" htmlFor={id}>
  301. { children }
  302. </label>
  303. </div>
  304. )
  305. };
  306. export default Admin;