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 (
+
+ )
+ }
+
+ 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
+
+
+
+
+ )
+ }
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ },
+ body: {
+ marginTop: 20,
+ maxWidth: 650,
+ marginBottom: 10,
+ backgroundColor: 'black',
+ padding: 40,
+ },
+ buttonStyle: {
+ marginTop: 40,
+ marginLeft: 0,
+ marginRight: 0,
+ marginBottom: 0,
+ },
+})
+
+const timelineHTMLStyles = 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: {
+ color: 'white',
+ fontFamily: 'Futura-MediumItalic',
+ fontSize: 16,
+ lineHeight: 30,
+ },
+ a: {
+ color: 'white',
+ fontFamily: 'Futura-Medium',
+ textDecorationLine: 'underline',
+ fontSize: 16,
+ lineHeight: 30,
+ },
+ red: {
+ color: '#f00',
+ fontWeight: 'bold',
+ fontFamily: 'Futura-MediumItalic',
+ fontSize: 16,
+ lineHeight: 24,
+ },
+ green: {
+ color: '#0f0',
+ fontWeight: 'bold',
+ fontFamily: 'Futura-MediumItalic',
+ fontSize: 16,
+ lineHeight: 24,
+ },
+ blue: {
+ color: '#08f',
+ fontWeight: 'bold',
+ fontFamily: 'Futura-MediumItalic',
+ fontSize: 16,
+ lineHeight: 24,
+ },
+})
+
--
cgit v1.2.3-70-g09d2