From 3e72bfa56c860826429a842f6c128d78d4a930db Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Thu, 1 Jun 2017 19:47:08 -0400 Subject: react-native-web port of fmf app --- client/src/lib/timeline/index.js | 164 +++++++++++++++++++++ client/src/lib/timeline/tickMarks.js | 192 +++++++++++++++++++++++++ client/src/lib/timeline/timelineEvent.js | 102 ++++++++++++++ client/src/lib/timeline/timelineFilter.js | 127 +++++++++++++++++ client/src/lib/timeline/timelineFull.js | 227 ++++++++++++++++++++++++++++++ client/src/lib/timeline/timelineHeader.js | 111 +++++++++++++++ 6 files changed, 923 insertions(+) create mode 100644 client/src/lib/timeline/index.js create mode 100644 client/src/lib/timeline/tickMarks.js create mode 100644 client/src/lib/timeline/timelineEvent.js create mode 100644 client/src/lib/timeline/timelineFilter.js create mode 100644 client/src/lib/timeline/timelineFull.js create mode 100644 client/src/lib/timeline/timelineHeader.js (limited to 'client/src/lib/timeline') diff --git a/client/src/lib/timeline/index.js b/client/src/lib/timeline/index.js new file mode 100644 index 0000000..3fde6df --- /dev/null +++ b/client/src/lib/timeline/index.js @@ -0,0 +1,164 @@ +import React, { Component } from 'react'; +import { + FlatList, + StyleSheet, + Text, + TouchableOpacity, + View, + ScrollView, +} from 'react-native'; + +import ScrollableContainer from '../components/scrollableContainer' +import Modal from '../components/modal' +import Container from '../components/container' +import ClearText from '../components/text' +import TimelineEvent from './timelineEvent' +import TimelineFull from './timelineFull' +import TimelineHeader from './timelineHeader' +import TimelineFilter from './timelineFilter' +import TickMarks from './tickMarks' + +export default class Timeline extends Component { + constructor(props) { + super() + this.state = { + events: props.events, + tag: props.tag, + introVisible: props.firstTime, + modalVisible: false, + filterVisible: false, + item: null, + + introVisible: false, +// modalVisible: true, +// item: props.events[2], + } + this.onPress = this.onPress.bind(this) + this.onFilter = this.onFilter.bind(this) + this.scrollToIndex = this.scrollToIndex.bind(this) + this.items = [] + } + onPress(item) { + this.setState({ + modalVisible: true, + item: item, + }) + } + onFilter(tag) { + let events; + if (!! tag && tag !== this.state.tag) { + events = this.props.events.filter((e) => e && e.keywords.includes(tag)) + } + else { + events = this.props.events + } + this.setState({ events, tag, modalVisible: false, filterVisible: false }) + } + scrollToIndex(index) { + // this.flatList._listRef.scrollToIndex(index) + // this.scrollable.scrollToIndex({index: index}) + const offset = this.items[index].ref.parentNode.offsetTop + this.scrollable.scrollView.scrollTo({ + y: offset, + x: 0, + animated: true, + }) + } + render() { + const heading = this.state.tag || 'History of Surveillance' + const items = this.state.events.map((item, i) => ( + this.items[i] = ref} key={item.id} onPress={this.onPress} item={item} /> + )) + + return ( + + this.scrollable = ref} heading={heading} headingOnPress={() => this.onFilter("")}> + {items} + + + + this.setState({ introVisible: false })} + /> + + + this.setState({ modalVisible: false })} + /> + + + ) + } +} + + + +// +// +// this.setState({ filterVisible: true })}> +// +// { !! this.state.tag ? 'FILTER' : '' } +// +// +// +// + +// +// this.setState({ filterVisible: false })} +// /> +// + +const styles = StyleSheet.create({ + wrapper: { + }, + modal: { + margin: 0, + padding: 0, + }, + headerModal: { + justifyContent: 'flex-start', + alignItems: 'center', + backgroundColor: 'rgb(0,0,0)', + margin: 0, + padding: 0, + }, + body: { + flexDirection: 'row', + height: 840, + }, + flatList: { + }, + container: { + flex: 1, + justifyContent: 'flex-start', + alignItems: 'center', + }, + sidebar: { + flex: 0, + }, + filterText: { + color: '#bbb', + textAlign: 'left', + fontSize: 12, + marginLeft: 4, + }, + filterActive: { + color: '#fff', + textDecorationLine: 'underline', + }, + filterInactive: { + color: '#bbb', + }, + tickMarks: { + flex: 0, + }, +}) diff --git a/client/src/lib/timeline/tickMarks.js b/client/src/lib/timeline/tickMarks.js new file mode 100644 index 0000000..c8bda29 --- /dev/null +++ b/client/src/lib/timeline/tickMarks.js @@ -0,0 +1,192 @@ +import React, { Component } from 'react' + +const isIphone = (navigator.userAgent.match(/iPhone/i)) || (navigator.userAgent.match(/iPod/i)) +const isIpad = (navigator.userAgent.match(/iPad/i)) +const isAndroid = (navigator.userAgent.match(/Android/i)) +const isMobile = isIphone || isIpad || isAndroid +const isDesktop = ! isMobile + +function getFirstTouch(f) { + return (e) => f(e.touches[0]) +} + +export default class TickMarks extends Component { + constructor(props) { + super() + + this.width = 45 + this.height = window.innerHeight - 160 + this.tag = "" + + this.onTouchStart = this.onTouchStart.bind(this) + this.onTouchMove = this.onTouchMove.bind(this) + this.onTouchEnd = this.onTouchEnd.bind(this) + } + + buildMarkers() { + this.yearTicks = [] + this.years = [] + + const height = this.height + const width = this.width + + const yPadding = fontStyle.fontSize + const heightRange = height - yPadding * 2 + + const startYear = 1800 + const endYear = 2018 + const yearStep = 10 + const yearMarkerStep = 50 + const yearDiff = endYear - startYear + + for (let year = startYear; year <= endYear; year += yearStep) { + let y = (year - startYear) / yearDiff * heightRange + yPadding + let isMarker = (year % yearMarkerStep) == 0 + let style = isMarker ? tickStyles.marker : tickStyles.year + this.yearTicks.push( + + ) + if (isMarker) { + this.years.push( + {year} + ) + } + } + this.yearsByOffset = [] + this.eventTicks = this.props.events.map((event, index) => { + if (event.date.match(/\D/)) { + return null + } + const year = parseInt(event.date) + const y = (year - startYear) / yearDiff * heightRange + yPadding + const style = tickStyles.event + if (year > 1800) { + this.yearsByOffset.push([y, Math.max(index-1, 0)]) + } + return ( + + ) + }).filter(e => !!e) + } + + scrollToYPosition(y) { + let index; + y -= this.svg.getBoundingClientRect().top + var foundOffset = this.yearsByOffset.some((pair) => { + if (y < pair[0]) { + index = pair[1] + return true + } + return false + }) + if (foundOffset) { + this.props.scrollToIndex(index) + } + } + + render() { + this.buildMarkers() + this.tag = this.props.tag + return ( + this.svg = ref} + > + {this.eventTicks} + {this.yearTicks} + {this.years} + + ) + } + + componentDidMount() { + if (isMobile) { + this.svg.addEventListener("touchstart", getFirstTouch(this.onTouchStart)) + this.svg.addEventListener("touchmove", getFirstTouch(this.onTouchMove)) + window.addEventListener("touchend", getFirstTouch(this.onTouchEnd)) + } + else { + this.svg.addEventListener("mousedown", this.onTouchStart) + this.svg.addEventListener("mousemove", this.onTouchMove) + window.addEventListener("mouseup", this.onTouchEnd) + } + } + + onTouchStart(e) { + this.dragging = true + this.scrollToYPosition(e.pageY) + } + onTouchMove(e) { + if (this.dragging || isMobile) { + this.scrollToYPosition(e.pageY) + } + } + onTouchEnd(e) { + this.dragging = false + } + +} + + +const styles = { + svg: { + position: 'fixed', + top: 105, + left: '2%', + zIndex: 2, + }, +} + +const tickStyles = { + year: { + width: 4, + stroke: '#bbb', + strokeWidth: 1, + }, + marker: { + width: 10, + stroke: '#888', + strokeWidth: 1 + }, + event: { + width: 10, + stroke: 'white', + strokeWidth: 2 + }, +} +const eventColors = { + 'Surveillance': 'rgb(0,64,255)', + 'Drones': 'rgb(255,0,0)', + 'Facial Recognition': 'rgb(0,255,0)', +} +const fontStyle = { + fontSize: 12, + yOffset: -3, + fill: '#bbb', +} diff --git a/client/src/lib/timeline/timelineEvent.js b/client/src/lib/timeline/timelineEvent.js new file mode 100644 index 0000000..903ceed --- /dev/null +++ b/client/src/lib/timeline/timelineEvent.js @@ -0,0 +1,102 @@ +import React, { Component } from 'react'; +import { + TouchableOpacity, + StyleSheet, + Image, + View +} from 'react-native'; + +import ClearText from '../components/text' + +const isIphone = (navigator.userAgent.match(/iPhone/i)) || (navigator.userAgent.match(/iPod/i)) +const isIpad = (navigator.userAgent.match(/iPad/i)) +const isAndroid = (navigator.userAgent.match(/Android/i)) +const isMobile = isIphone || isIpad || isAndroid +const isDesktop = ! isMobile + +const imageHeight = isMobile ? 70 : 100 +const imageWidth = isMobile ? 100 : 150 + +export default class TimelineEvent extends Component { + constructor() { + super() + } + render() { + const item = this.props.item + let image; + if (item.image && item.image.uri) { + const originalWidth = Number(item.image.width) + const originalHeight = Number(item.image.height) + let height = originalHeight > imageHeight ? imageHeight : originalHeight + let width = originalWidth * height / originalHeight + if (width > imageWidth) { + width = imageWidth + height = originalHeight * imageWidth / originalWidth + } + if (isNaN(width) || isNaN(height)) { + console.log(width, height, item.image.uri) + } + image = + } else { + image = + } + return ( + this.props.onPress(this.props.item) }> +
this.ref = ref} style={{flex: 0}}>
+ + + {item.date} + + {image} + + {item.title} + + +
+ ) + } +} + +const styles = StyleSheet.create({ + item: { + flex: 1, + width: '80%', + justifyContent: 'flex-start', + alignItems: 'center', + flexDirection: 'row', + padding: 10, + marginBottom: 10, + minHeight: 100, + }, + dateContainer: { + width: '30%', + justifyContent: 'flex-start', + alignItems: 'center', + paddingRight: 10, + paddingLeft: 30, + }, + imageContainer: { + width: '40%', + justifyContent: 'flex-start', + alignItems: 'center', + marginRight: 10, + }, + titleContainer: { + width: '30%', + justifyContent: 'flex-start', + alignItems: 'center', + paddingLeft: 10, + }, + title: { + textAlign: 'center', + fontWeight: 'bold', + }, + date: { + textAlign: 'center', + }, +}) diff --git a/client/src/lib/timeline/timelineFilter.js b/client/src/lib/timeline/timelineFilter.js new file mode 100644 index 0000000..c6da98a --- /dev/null +++ b/client/src/lib/timeline/timelineFilter.js @@ -0,0 +1,127 @@ +import React, { Component } from 'react'; +import { + ScrollView, + StyleSheet, + TouchableOpacity, + Image, + View, + Text, + RefreshControl, +} from 'react-native'; + +import ClearText from '../components/text' +import Heading from '../components/heading' +import Definition from '../components/definition' +import Close from '../components/close' + +export default class TimelineFilter extends Component { + constructor(props) { + super() + this.onRefresh = this.onRefresh.bind(this) + this.buildCounts(props.events) + } + buildCounts(events){ + const lookup = {} + events.forEach((event) => { + event.keywords.forEach((t) => { + lookup[t] = lookup[t] || 0 + lookup[t] += 1 + }) + }) + + this.tags = Object.keys(lookup) + .map((t) => [t, lookup[t]]) + .sort((a,b) => a[1] t[1] > 1) + .reverse() + } + buildTagItems() { + const maxFontSize = 22 + const minFontSize = 12 + const fontSizeDiff = maxFontSize - minFontSize + const maxCount = this.tags[2][1] + const selectedTag = this.props.tag + + return this.tags.map((pair, i) => { + const tag = pair[0] + const count = pair[1] + const fontSize = Math.min(maxCount, count) / maxCount * (fontSizeDiff) + minFontSize + const textDecorationLine = tag === selectedTag ? 'underline' : 'none' + const color = (!! selectedTag && tag !== selectedTag) ? '#bbb' : '#fff' + + return ( + this.props.onFilter(tag)} + > + {tag} + + ) + }) + + } + onRefresh() { + this.props.onClose() + } + render() { + const tagItems = this.buildTagItems() + const refreshControl = ( + + ) + + return ( + + + CATEGORIES + {tagItems} + + + + ) + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'flex-start', + alignItems: 'flex-start', + }, + heading: { + marginLeft: 0, + }, + scrollView: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + }, + body: { + backgroundColor: 'black', + height: '100%', + }, + tagItems: { + flexWrap: 'wrap', + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'row', + padding: 5, + }, + link: { + marginHorizontal: 2, + marginVertical: 2, + lineHeight: 16, + }, +}) diff --git a/client/src/lib/timeline/timelineFull.js b/client/src/lib/timeline/timelineFull.js new file mode 100644 index 0000000..a004987 --- /dev/null +++ b/client/src/lib/timeline/timelineFull.js @@ -0,0 +1,227 @@ +import React, { Component } from 'react'; +import { + ScrollView, + StyleSheet, + TouchableOpacity, + Image, + View, + RefreshControl, +} from 'react-native'; + +import HTMLView from 'react-native-htmlview' +import htmlStyles from '../components/htmlStyles' + +import ClearText from '../components/text' +import Heading from '../components/heading' +import Definition from '../components/definition' +import Close from '../components/close' + +export default class TimelineFull extends Component { + constructor() { + super() + this.onRefresh = this.onRefresh.bind(this) + this.onPickTag = this.onPickTag.bind(this) + } + onRefresh() { + this.props.onClose() + } + onPickTag(tag) { + this.props.onFilter(tag) + } + render() { + const item = this.props.item + let image, links; + if (! item) { + return ( ) + } + + if (item.image) { + const caption = item.credit ? ( + {item.credit} + ) : ( + + ) + const originalWidth = Number(item.image.width) + const originalHeight = Number(item.image.height) + const height = originalHeight > 450 ? 450 : originalHeight + const width = originalWidth * height / originalHeight + image = ( + + + + + {caption} + + ) + } else { + image = ( ) + } + + if (item.links.length) { + const linkItems = item.links.map((link, i) => { + const url = link.uri + let name = link.text + if (! name || name.match(/Link Text/i)) { + name = linkTextFromUrl(url) + } + return ( + this.props.onLinkPress(url)}> + {name} + + ) + }) + links = ( + {linkItems} + ) + } + + const tags = item.keywords.map((tag, i) => { + return `${tag}` + }).join(', ') + + const description = '

' + item.description + '

' + + return ( + + + {image} + {item.title} + + + + + + {item.date} + {item.medium} + + this.onPickTag(item.category)}> + {item.category} + + + + ' + tags + '

'} stylesheet={tagHTMLStyles} onLinkPress={this.onPickTag} /> +
+ {links} +
+
+
+ +
+ ) + } +} + + +function linkTextFromUrl (url) { + const url_parts = url.split('/') + const domain = url_parts[2] + const terms = domain.split('.') + const len = terms.length + let term = (len > 2 && terms[len-1].length == 2) ? terms[len-3] : terms[len-2] + if (term == 'wikipedia') { + term += url_parts[4].replace(/\#.*/,'').replace('_', ' ') + } + return capitalize(term) +} +function capitalize (s){ + return s.charAt(0).toUpperCase() + s.slice(1) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + scrollView: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'black', + }, + item: { + maxWidth: 1000, + padding: 40, + }, + imageContainer: { + width: '100%', + marginBottom: 10, + alignItems: 'center', + }, + imageWrapper: { + width: '100%', + marginBottom: 5, + alignItems: 'center', + }, + caption: { + fontSize: 12, + }, + + bodyContainer: { + left: '0%', + width: '60%', + paddingRight: 50, + }, + metadataContainer: { + position: 'absolute', + left: '60%', + width: '40%', + }, + title: { + textAlign: 'left', + fontWeight: 'bold', + fontSize: 18, + }, + date: { + textAlign: 'left', + }, + description: { + flex: 1, + flexDirection: 'column', + }, + link: { + textDecorationLine: 'underline', + textAlign: 'left', + }, + tag: { + marginRight: 5, + }, +}) + +const tagHTMLStyles = StyleSheet.create({ + p: { + color: 'white', + fontFamily: 'Futura-Medium', + textAlign: 'justify', + fontSize: 16, + lineHeight: 30, + }, + b: { + fontFamily: 'Futura-MediumItalic', + color: 'white', + fontSize: 16, + lineHeight: 30, + }, + i: { + fontFamily: 'Futura-MediumItalic', + color: 'white', + fontSize: 16, + lineHeight: 30, + }, + a: { + color: 'white', + fontFamily: 'Futura-Medium', + textDecorationLine: 'underline', + fontSize: 16, + lineHeight: 30, + }, +}) + diff --git a/client/src/lib/timeline/timelineHeader.js b/client/src/lib/timeline/timelineHeader.js new file mode 100644 index 0000000..559ace9 --- /dev/null +++ b/client/src/lib/timeline/timelineHeader.js @@ -0,0 +1,111 @@ +import React, { Component } from 'react'; +import { + StyleSheet, + TouchableOpacity, + Text, + View, + ScrollView, +} from 'react-native'; +import HTMLView from 'react-native-htmlview' + +import htmlStyles from '../components/htmlStyles' +import Heading from '../components/heading' +import ClearText from '../components/text' +import Button from '../components/button' +import Close from '../components/close' + +export default class TimelineHeader extends Component { + constructor(props) { + super() + } + render() { + if (!! this.props.tag) { + return ( + + ) + } + else { + const body = '

' + this.props.content.body + '

' + return ( + + + HISTORY OF SURVEILLANCE + +