import React, { useState } from 'react'; import { Container, Badge, FormGroup, Label, Input, Button, Collapse, Table, ListGroup, ListGroupItem } from 'reactstrap'; import Paper, { Path, PaperScope, Point, Raster, Color } from 'paper'; import { without, append, isNil, identity, includes, is } from 'ramda'; import Axios from 'axios'; import VALUES, { AgeVal, GenderVal, NonNativeVal, Filters, FilterVal, FilterStatus, EducationVal } from './services'; import FileDownload from 'js-file-download'; import map from './merseyside-nobg-white.png'; import './Admin.css'; const BACKEND = process.env.REACT_APP_BACKEND || '/backend/'; type PersonalInformation = { age: AgeVal, gender: GenderVal, genderCustom: string, birthPlace: string, currentPlace: string, levelEducation: EducationVal[], nonNative: NonNativeVal, } type Result = { personalInformation: PersonalInformation, canvas: OriginalCanvasData[], canvasSize: { width: number, height: number }, email: string, id: number, } interface StateData { id: number, personalInformation: PersonalInformation, canvas: CanvasData[], // group: paper.Group, email: string }; interface FormPath { name: string, soundExample: string, associations: string[], correctness: number, friendliness: number, pleasantness: number, trustworthiness: number } interface ShapeData extends FormPath, PersonalInformation { id: number } type OriginalCanvasData = { form: FormPath, path: string, } type CanvasData = { form: FormPath, path: paper.Path, label: paper.PointText }; type AdminState = { original: StateData[], data: StateData[], canvas?: paper.PaperScope, focusedResponse?: StateData, path?: paper.Path, selectedArea?: paper.Item[] }; const mkPathLabel = (pathName: string, path: paper.Path) => { return new Paper.PointText({ fillColor: '#4b505a', justification: 'center', fontWeight: 'bold', fontSize: '16px', point: path.bounds.center, content: pathName, visible: false }) } const COLORS = [ '#ffc6bc', '#fedea2', '#fff9b8', '#d3dbb2', '#a7d3d2', '#efe3f3', '#c4d0f5', '#c0ba98', ]; const mkBgColor = (color: string) => { const _color = new Paper.Color(color); _color.alpha = 0.2; return _color; }; const mkPath = (pathData: string, i: number, scale: number, data: ShapeData): paper.Path => { const color = COLORS[i % COLORS.length]; const _path = new Path(); _path.importJSON(pathData); _path.strokeColor = new Color(color); _path.strokeWidth = 2; _path.fillColor = mkBgColor(color) _path.selected = false; _path.scale(scale, new Point(0, 0)); _path.data = data; return _path; } class Admin extends React.Component<{}, AdminState> { constructor(props: any) { super(props); this.state = { canvas: undefined, original: [], data: [], focusedResponse: undefined, path: undefined, selectedArea: undefined }; } createPath = (canvas: paper.PaperScope) => (event: any) => { if(this.state.path === undefined) { this.toggleDrawings(this.state.data, false); let path = new canvas.Path({ strokeColor: new Color('red'), strokeWidth: 5, }); path.bringToFront(); path.add(event.point); this.setState({ path }); } } addPoint = (event: any) => { if (this.state.path && this.state.selectedArea === undefined) { this.state.path?.add(event.point) } }; getItems = (canvas: paper.PaperScope) => () => { const { path, selectedArea } = this.state; if (path && !selectedArea) { path.add(path.firstSegment); path.closePath(); path.simplify(); const items = canvas.project.activeLayer.getItems({ match: (value: paper.Item) => { return value.data.id && path.intersects(value); }, class: Path, }); items.forEach(item => { item.visible = true }); this.setState({ selectedArea: items || [] }) } } toggleDrawings = (data: StateData[], visibility: boolean) => { data.forEach(result => { result.canvas.forEach(({ path }) => { path.visible = visibility; }) }) } mkDrawingTool = (canvas: paper.PaperScope) => { let Tool = new canvas.Tool(); Tool.onMouseDown = this.createPath(canvas); Tool.onMouseDrag = this.addPoint; Tool.onMouseUp = this.getItems(canvas); } componentDidMount = () => { const canvas = new PaperScope(); canvas.setup('vom-admin-canvas'); canvas.view.viewSize.height = canvas.view.size.width * 1.25; this.mkDrawingTool(canvas); Axios.get(BACKEND, { headers: { 'X-Token': 'secret-potato', } }).then(response => { const _data = response.data.map((result, index) => { const item = { ...result, canvas: result.canvas.map((item, i) => { const pathData: ShapeData = { id: result.id, ...result.personalInformation, ...item.form }; const _path = mkPath(item.path, index, canvas.view.size.width / result.canvasSize.width, pathData); const _text = mkPathLabel(`${item.form.name} (${i})`, _path); return { path: _path, label: _text, form: item.form } }), }; return item; }) const raster = new Raster(map); raster.onLoad = () => { raster.position = canvas.view.center; raster.size = canvas.view.viewSize; raster.bringToFront(); } this.setState({ canvas, original: _data, data: _data, }) }) } isNotEducational = (value: AgeVal | GenderVal | NonNativeVal | string | EducationVal[]): value is AgeVal | GenderVal | NonNativeVal | string => { return is(String, value); } applyFilters = (filters: Filters) => { const results = this.state.original.filter( result => { const matches = VALUES.FILTER_KEYS.map(filterKey => { const filterValues = filters[filterKey]; const resultValue = result.personalInformation[filterKey]; if (filterValues) { if (this.isNotEducational(resultValue)) { return !isNil(filterValues.find(v => { return v === resultValue })); } else { return filterValues.some(v => includes(v, resultValue)) } } else { return true; } }).every(identity); if (matches) { result.canvas.forEach(({ path}) => { path.visible = true; }) return true } else { result.canvas.forEach(({path}) => { path.visible = false; }) return false; } } ) this.setState({ data: results, focusedResponse: undefined, }) } focusPath = (id: number) => { this.state.data.forEach(result => { if (result.id === id) { result.canvas.forEach(({path}) => { path.visible = true; }) this.setState({ focusedResponse: result }) } else { result.canvas.forEach(({ path}) => { path.visible = false; }) } }); } clearFocus = () => { this.state.data.forEach(result => { result.canvas.forEach(({path}) => { path.visible = true; }) }) this.setState({ focusedResponse: undefined }) } clearDrawing = () => { this.state.data.forEach(result => { result.canvas.forEach(({path}) => { path.visible = true; }) }); this.state.path?.remove(); this.setState({ path: undefined, selectedArea: undefined, }) } downloadData = () => { Axios.get(`${BACKEND}/csv`, { headers: { 'X-Token': 'secret-potato', } }).then(response => { FileDownload(response.data, 'voices-of-merseyside.csv') }) } render() { return (

Administration panel

total responses: {this.state.original.length}

showing: {this.state.data.length}

this.downloadData() }>download

{ this.state.selectedArea ? ( <>

drawings in area: {this.state.selectedArea.length} this.clearDrawing() }>clear drawing

) : null }
{ this.state.focusedResponse ? (

Showing:

) : null }

Results

{ this.state.selectedArea ? ( ) : ( ) }
) } } type SelectedAreaTotals = { age: { [k: string]: number, '1': number, '2': number, '3': number, '4': number, '5': number, '6': number }, gender: { [k: string]: number, 'female': number, 'male': number, 'other': number }, levelEducation: { [k: string]: number, '1': number, '2': number, '3': number, '4': number } } const SelectedAreaTable: React.FunctionComponent<{items: paper.Item[]}> = ({ items }) => { const init = { age: { '1': 0, '2': 0, '3': 0, '4': 0, '5': 0, '6': 0 }, gender: { 'female': 0, 'male': 0, 'other': 0 }, levelEducation: { '1': 0, '2': 0, '3': 0, '4': 0 } }; const [totals] = useState(items.reduce((totals, item) => { const data = item.data as ShapeData; const lvlEd = data.levelEducation ? data.levelEducation.reduce((tot, lvl) => { return { ...tot, [lvl]: totals.levelEducation[lvl] + 1 } }, {}) : {}; return data.age ? { age: { ...totals.age, [data.age]: totals.age[data.age] + 1 }, gender: { ...totals.gender, [data.gender]: totals.gender[data.gender] + 1 }, levelEducation: { ...totals.levelEducation, ...lvlEd } } : totals; }, { ...init }) || init) return ( <>
Age
{ Object.keys(totals.age).map((age, key) => ( ))}
16-17 18-25 26-45 46-65 66-75 75+
{totals.age[age]}
Gender
{ Object.keys(totals.gender).map((gender, key) => ( )) }
female male other
{totals.gender[gender]}
Level of Education
{ Object.keys(totals.levelEducation).map((levelEducation, key) => ( )) }
high school or lower bachelors masters doctorate
{totals.levelEducation[levelEducation]}
) } type TableProps = { data: StateData[], focusPath: (id: number) => void, clearFocus: () => void, }; const DrawingsTable: React.FunctionComponent<{items: paper.Item[]}> = ({ items }) => { return ( { items.map(({ data }, key) => { const d = data as ShapeData; return d.gender ? ( ) : null }) }
# age G education birth place current place NN name example associations C F P T
{d.id} {VALUES.AGE[d.age] || ''} {d.gender[0] || ''} {d.levelEducation.map(e => VALUES.EDUCATION[e] || '').join(', ')} {d.birthPlace} {d.currentPlace} {VALUES.NON_NATIVE[d.nonNative] || '-'} {d.name} {d.soundExample || '-'} {d.associations.join(', ')} {d.correctness} {d.friendliness} {d.pleasantness} {d.trustworthiness}
) } const ResultsTable: React.FunctionComponent = ({ data, focusPath, clearFocus }) => { const [ focused, setFocused ] = useState(); return ( <>
{ data.map(({ id, personalInformation, canvas }) => { return canvas.map(({ form }, i) => ( { focusPath(id); setFocused(id) }} className={ `${i === 0 ? 'main-row' : ''} ${id === focused ? 'data-focused' : ''} ` } > { i === 0 ? ( <> ) : null } )) }) }
# age G education birth place current place NN # name example associations C F P T
{id} {VALUES.AGE[personalInformation.age]} {personalInformation.gender[0]} {personalInformation.levelEducation.map(e => VALUES.EDUCATION[e])} {personalInformation.birthPlace} {personalInformation.currentPlace} {VALUES.NON_NATIVE[personalInformation.nonNative] || '-'}{i} {form.name} {form.soundExample || '-'} {form.associations.join(', ')} {form.correctness} {form.friendliness} {form.pleasantness} {form.trustworthiness}
) } const FilterPanel: React.FunctionComponent<{ applyFilters: (filters: Filters) => void }> = ({ applyFilters }) => { const [filters, setFilters] = useState({ ...VALUES.FILTER, nonNative: undefined }); const [activeFilters, setActiveFilters] = useState({ age: true, gender: true, levelEducation: true, nonNative: false, }); const isFilterActive = (field: FilterVal) => { return activeFilters[field] }; const activateFilter = (field: FilterVal) => ({ target }: React.ChangeEvent) => { const _filters = { ...activeFilters, [field]: target.checked } if (target.checked) { handleFilter({ ...filters, [field]: VALUES.FILTER[field] }); } else { handleFilter({ ...filters, [field]: undefined }) } setActiveFilters(_filters); } const selectAll = () => { handleFilter({ ...VALUES.FILTER, nonNative: undefined }) } const isChecked = (field: FilterVal, value: string) => { const values = filters[field]; if (values){ return includes(value, values); } else { return false } } const handleCheck = (field: FilterVal, value: string) => ({ target }: React.ChangeEvent) => { const _filters = { ...filters, [field]: target.checked ? append(value, filters[field] || []) : without([ value ], filters[field] || []) }; handleFilter(_filters) } const handleFilter = (_filters: Filters) => { applyFilters(_filters); setFilters(_filters) } return (

Filters

Age
{ (Object.keys(VALUES.AGE) as AgeVal[]).map(value => ( )) }
Gender
{ VALUES.GENDER.map(value => ( )) }
Level of education
{ (Object.keys(VALUES.EDUCATION) as EducationVal[]).map(value => ( )) }
Non natives
{ (Object.keys(VALUES.NON_NATIVE) as NonNativeVal[]).map(value => ( )) }
) } const Switch: React.FunctionComponent, HTMLInputElement>> = ({ id, children , ...props}) => { return (
) }; const ViewResponse: React.FunctionComponent<{response: StateData}> = ({ response }) => { const { personalInformation, email, canvas } = response; return (
Age: {VALUES.AGE[personalInformation.age]} Gender: {personalInformation.gender} Education: {personalInformation.levelEducation.map(e => VALUES.EDUCATION[e]).join(', ')} Birth Place: {personalInformation.birthPlace} Current Place: {personalInformation.currentPlace} Non Native?: {VALUES.NON_NATIVE[personalInformation.nonNative] || '-'} email: {email || '-'} { canvas.map(({form}, i) => ( Accent name: {form.name} ({i})
Example: {form.soundExample}
Associations: {form.associations.join(', ')}
Correctness: {form.correctness}
Friendliness: {form.friendliness}
Pleasantness: {form.pleasantness}
Trustworthiness: {form.trustworthiness}
)) }
) } export default Admin;