From e1de58e7013f798c1a25b386809b497ea9e93918 Mon Sep 17 00:00:00 2001 From: Zac Ioannidis Date: Fri, 20 Nov 2020 06:35:57 +0000 Subject: [PATCH] Added generators for card layouts. (#182) * Added generators for card layouts. These are optionally defined in the timemap config * Removed US2020-specific layout generation - now it's being specified in the config --- src/common/card.js | 31 ++++++ src/components/CardStack.jsx | 148 +++++++--------------------- src/reducers/validate/validators.js | 42 ++++---- src/store/initial.js | 24 +++-- 4 files changed, 101 insertions(+), 144 deletions(-) create mode 100644 src/common/card.js diff --git a/src/common/card.js b/src/common/card.js new file mode 100644 index 0000000..d1743f9 --- /dev/null +++ b/src/common/card.js @@ -0,0 +1,31 @@ +// Sensible defaults for generating a basic card layout +// based on the example Timemap datasheet. +const basic = ({ event }) => { + return [ + [ + { + kind: 'date', + title: 'Incident Date', + value: event.datetime || event.date || `` + }, + { + kind: 'text', + title: 'Location', + value: event.location || `—` + } + ], + [{ kind: 'line-break', times: 0.4 }], + [ + { + kind: 'text', + title: 'Summary', + value: event.description || ``, + scaleFont: 1.1 + } + ] + ] +} + +export default { + basic +} diff --git a/src/components/CardStack.jsx b/src/components/CardStack.jsx index eda420e..2f85c55 100644 --- a/src/components/CardStack.jsx +++ b/src/components/CardStack.jsx @@ -1,11 +1,8 @@ import React from 'react' import { connect } from 'react-redux' -import * as selectors from '../selectors' -import { - // calculateColorPercentages, - getFilterIdxFromColorSet -} from '../common/utilities' +import * as selectors from '../selectors' +import { getFilterIdxFromColorSet } from '../common/utilities' // import Card from './Card.jsx' import { Card } from '@forensic-architecture/design-system/react' import copy from '../common/data/copy.json' @@ -29,7 +26,8 @@ class CardStack extends React.Component { scrollToCard () { const duration = 500 const element = this.refCardStack.current - const cardScroll = this.refs[this.props.narrative.current].current.offsetTop + const cardScroll = this.refs[this.props.narrative.current].current + .offsetTop let start = element.scrollTop let change = cardScroll - start @@ -42,9 +40,9 @@ class CardStack extends React.Component { // d = duration Math.easeInOutQuad = function (t, b, c, d) { t /= d / 2 - if (t < 1) return c / 2 * t * t + b + if (t < 1) return (c / 2) * t * t + b t -= 1 - return -c / 2 * (t * (t - 2) - 1) + b + return (-c / 2) * (t * (t - 2) - 1) + b } const animateScroll = function () { @@ -58,104 +56,29 @@ class CardStack extends React.Component { renderCards (events, selections) { // if no selections provided, select all - if (!selections) { selections = events.map(e => true) } + if (!selections) { + selections = events.map((e) => true) + } this.refs = [] return events.map((event, idx) => { const thisRef = React.createRef() this.refs[idx] = thisRef - let precision - switch (event.location_precision) { - case `Generalised`: - precision = `No location data` - break - case `Estimated`: - precision = `Precise location estimated` - break - case `Self-reported`: - precision = `Location reported by witness` - break - case `Confirmed`: - default: - precision = null - break - } - - return ( ({ - text: association, - color: getFilterIdxFromColorSet(association, this.props.coloringSet) >= 0 ? this.props.colors[getFilterIdxFromColorSet(association, this.props.coloringSet)] : null, - normalCursor: true - })) - }, - { - kind: 'button', - title: 'Against', - value: event.associations.slice(-1).map(category => ({ - text: category, - color: null, - normalCursor: true - })) - } - ], - [{ kind: 'line-break', times: 0.2 }], - [ - { - kind: 'list', - title: 'Law Enforcement Agencies', - value: event.le_agencys - } - ], - [ - { kind: 'text', title: 'Name of reporter(s)', value: event.journalist_name }, - { kind: 'text', title: 'Network', value: event.news_organisation } - ], - [ - { - kind: event.hide_source === 'FALSE' ? 'button' : 'markdown', - title: 'Sources', - value: event.hide_source === 'FALSE' ? event.links.map((href, idx) => ({ text: `Source ${idx + 1}`, href, color: null, onClick: () => window.open(href, '_blank') })) : 'Source hidden to protect the privacy and dignity of civilians. Read more [here](https://staging.forensic-architecture.org/wp-content/uploads/2020/09/2020.14.09-FA-Bcat-Mission-Statement.pdf).' - } - ] - // [{ kind: "text", title: "Category", value: "Press attack" }], - ]} - language={this.props.language} - isLoading={this.props.isLoading} - isSelected={selections[idx]} - // getNarrativeLinks={this.props.getNarrativeLinks} - // getCategoryGroup={this.props.getCategoryGroup} - // getCategoryColor={this.props.getCategoryColor} - // getCategoryLabel={this.props.getCategoryLabel} - // onViewSource={this.props.onViewSource} - // onHighlight={this.props.onHighlight} - // onSelect={this.props.onSelect} - // features={this.props.features} - />) + return ( + + ) }) } @@ -172,8 +95,7 @@ class CardStack extends React.Component { const { narrative } = this.props const showing = narrative.steps - const selections = showing - .map((_, idx) => (idx === narrative.current)) + const selections = showing.map((_, idx) => idx === narrative.current) return this.renderCards(showing, selections) } @@ -187,7 +109,9 @@ class CardStack extends React.Component { className='card-stack-header' onClick={() => this.props.onToggleCardstack()} > - +

{`${this.props.selected.length} ${headerLang}`}

@@ -198,21 +122,19 @@ class CardStack extends React.Component { renderCardStackContent () { return (
-
    - {this.renderSelectedCards()} -
+
    {this.renderSelectedCards()}
) } renderNarrativeContent () { return ( -
-
    - {this.renderNarrativeCards()} -
+
    {this.renderNarrativeCards()}
) } @@ -227,8 +149,7 @@ class CardStack extends React.Component {
{this.renderCardStackHeader()} {this.renderCardStackContent()} @@ -240,8 +161,7 @@ class CardStack extends React.Component { id='card-stack' ref={this.refCardStack} className={`card-stack narrative-mode - ${isCardstack ? '' : ' folded'}` - } + ${isCardstack ? '' : ' folded'}`} style={{ height }} > {this.renderNarrativeContent()} diff --git a/src/reducers/validate/validators.js b/src/reducers/validate/validators.js index a6c24c4..a9aec5c 100644 --- a/src/reducers/validate/validators.js +++ b/src/reducers/validate/validators.js @@ -9,9 +9,9 @@ import shapeSchema from './shapeSchema' import { calcDatetime, capitalize } from '../../common/utilities' /* -* Create an error notification object -* Types: ['error', 'warning', 'good', 'neural'] -*/ + * Create an error notification object + * Types: ['error', 'warning', 'good', 'neural'] + */ function makeError (type, id, message) { return { type: 'error', @@ -27,11 +27,15 @@ function isValidDate (d) { function findDuplicateAssociations (associations) { const seenSet = new Set([]) const duplicates = [] - associations.forEach(item => { + associations.forEach((item) => { if (seenSet.has(item.id)) { duplicates.push({ id: item.id, - error: makeError('Association', item.id, 'association was found more than once. Ignoring duplicate.') + error: makeError( + 'Association', + item.id, + 'association was found more than once. Ignoring duplicate.' + ) }) } else { seenSet.add(item.id) @@ -41,8 +45,8 @@ function findDuplicateAssociations (associations) { } /* -* Validate domain schema -*/ + * Validate domain schema + */ export function validateDomain (domain, features) { const sanitizedDomain = { events: [], @@ -79,13 +83,13 @@ export function validateDomain (domain, features) { } function validateArray (items, domainKey, schema) { - items.forEach(item => { + items.forEach((item) => { validateArrayItem(item, domainKey, schema) }) } function validateObject (obj, domainKey, itemSchema) { - Object.keys(obj).forEach(key => { + Object.keys(obj).forEach((key) => { const vl = obj[key] const result = Joi.validate(vl, itemSchema) if (result.error !== null) { @@ -113,13 +117,10 @@ export function validateDomain (domain, features) { validateObject(domain.shapes, 'shapes', shapeSchema) // NB: [lat, lon] array is best format for projecting into map - sanitizedDomain.shapes = sanitizedDomain.shapes.map(shape => ({ + sanitizedDomain.shapes = sanitizedDomain.shapes.map((shape) => ({ name: shape.name, - points: shape.items.map(coords => ( - coords.replace(/\s/g, '').split(',') - )) - }) - ) + points: shape.items.map((coords) => coords.replace(/\s/g, '').split(',')) + })) const duplicateAssociations = findDuplicateAssociations(domain.associations) // Duplicated associations @@ -137,7 +138,14 @@ export function validateDomain (domain, features) { event.id = idx event.datetime = calcDatetime(event.date, event.time) if (!isValidDate(event.datetime)) { - discardedDomain['events'].push({ ...event, error: makeError('events', event.id, `Invalid date. It's been dropped, as otherwise timemap won't work as expected.`) }) + discardedDomain['events'].push({ + ...event, + error: makeError( + 'events', + event.id, + `Invalid date. It's been dropped, as otherwise timemap won't work as expected.` + ) + }) return false } return true @@ -146,7 +154,7 @@ export function validateDomain (domain, features) { sanitizedDomain.events.sort((a, b) => a.datetime - b.datetime) // Message the number of failed items in domain - Object.keys(discardedDomain).forEach(disc => { + Object.keys(discardedDomain).forEach((disc) => { const len = discardedDomain[disc].length if (len) { sanitizedDomain.notifications.push({ diff --git a/src/store/initial.js b/src/store/initial.js index 88f1cc5..a1e6f40 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -1,5 +1,6 @@ import { mergeDeepLeft } from 'ramda' import global, { colors } from '../common/global' +import generateCardLayout from '../common/card' const isSmallLaptop = window.innerHeight < 800 const initial = { @@ -46,7 +47,7 @@ const initial = { sites: true } }, - isMobile: (/Mobi/.test(navigator.userAgent)), + isMobile: /Mobi/.test(navigator.userAgent), language: 'en-US', map: { anchor: [31.356397, 34.784818], @@ -54,7 +55,10 @@ const initial = { minZoom: 2, maxZoom: 16, bounds: null, - maxBounds: [[180, -180], [-180, 180]] + maxBounds: [ + [180, -180], + [-180, 180] + ] }, cluster: { radius: 30, @@ -71,14 +75,8 @@ const initial = { contentHeight: isSmallLaptop ? 160 : 200, width_controls: 100 }, - range: [ - new Date(2001, 2, 23, 12), - new Date(2021, 2, 23, 12) - ], - rangeLimits: [ - new Date(1, 1, 1, 1), - new Date() - ], + range: [new Date(2001, 2, 23, 12), new Date(2021, 2, 23, 12)], + rangeLimits: [new Date(1, 1, 1, 1), new Date()], zoomLevels: [ { label: '20 years', duration: 10512000 }, { label: '2 years', duration: 1051200 }, @@ -99,7 +97,8 @@ const initial = { }, cover: { title: 'project title', - description: 'A description of the project goes here.\n\nThis description may contain markdown.\n\n# This is a large title, for example.\n\n## Whereas this is a slightly smaller title.\n\nCheck out docs/custom-covers.md in the [Timemap GitHub repo](https://github.com/forensic-architecture/timemap) for more information around how to specify custom covers.', + description: + 'A description of the project goes here.\n\nThis description may contain markdown.\n\n# This is a large title, for example.\n\n## Whereas this is a slightly smaller title.\n\nCheck out docs/custom-covers.md in the [Timemap GitHub repo](https://github.com/forensic-architecture/timemap) for more information around how to specify custom covers.', exploreButton: 'EXPLORE' }, loading: false @@ -135,8 +134,7 @@ const initial = { } }, card: { - order: [[`renderTime`, `renderLocation`], [`renderSummary`], [`renderCustomFields`]], - extra: [[`renderSources`]] + layout: ({ event }) => generateCardLayout['basic']({ event }) }, coloring: { maxNumOfColors: 4,