summaryrefslogtreecommitdiff
path: root/client/src/lib/views
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/lib/views')
-rw-r--r--client/src/lib/views/contact.js307
-rw-r--r--client/src/lib/views/credits.js34
-rw-r--r--client/src/lib/views/home.js98
-rw-r--r--client/src/lib/views/information.js114
-rw-r--r--client/src/lib/views/livestream.js134
-rw-r--r--client/src/lib/views/nav.js119
-rw-r--r--client/src/lib/views/privacy.js33
7 files changed, 839 insertions, 0 deletions
diff --git a/client/src/lib/views/contact.js b/client/src/lib/views/contact.js
new file mode 100644
index 0000000..74f79b5
--- /dev/null
+++ b/client/src/lib/views/contact.js
@@ -0,0 +1,307 @@
+import React, { Component } from 'react';
+import {
+ ActivityIndicator,
+ KeyboardAvoidingView,
+ TouchableHighlight,
+ TouchableWithoutFeedback,
+ StyleSheet,
+ TextInput,
+ Keyboard,
+ Text,
+ View
+} from 'react-native';
+
+import hosts from '../db/hosts'
+
+const validator = require("email-validator")
+
+import ScrollableContainer from '../components/scrollableContainer'
+import ClearText from '../components/text'
+import Heading from '../components/heading'
+import Button from '../components/button'
+import CheckBox from '../components/checkbox'
+import Modal from '../components/modal'
+
+export default class Contact extends Component {
+ constructor() {
+ super()
+ this.state = {
+ name: '',
+ email: '',
+ message: '',
+ track: false,
+ publish: false,
+ sending: false,
+ done: false,
+ }
+ this.setName = this.setName.bind(this)
+ this.setEmail = this.setEmail.bind(this)
+ this.setTrack = this.setTrack.bind(this)
+ this.setPublish = this.setPublish.bind(this)
+ this.setMessage = this.setMessage.bind(this)
+ this.send = this.send.bind(this)
+ this.reset = this.reset.bind(this)
+ }
+ setName(name) {
+ this.setState({ name, error: false })
+ }
+ setEmail(email) {
+ this.setState({ email, error: false })
+ }
+ setTrack(track) {
+ this.setState({ track: ! track })
+ }
+ setPublish(publish) {
+ this.setState({ publish: ! publish })
+ }
+ setMessage(message) {
+ this.setState({ message: message })
+ }
+ send() {
+ var isValidEmail = validator.validate(this.state.email)
+ if (! isValidEmail) {
+ // return this.setState({ error: true })
+ this.setState({ sending: true, track: false })
+ }
+ else {
+ this.setState({ sending: true })
+ }
+// fetch('POST', hosts.feedback, {
+// 'Content-Type' : 'multipart/form-data',
+// }, [
+// { name: 'name', data: this.state.name },
+// { name: 'email', data: this.state.email },
+// { name: 'message', data: this.state.message },
+// { name: 'track', data: this.state.track },
+// { name: 'publish', data: this.state.publish },
+// { name: 'secret', data: 'iNr51NbvUDqbBzyray7gdZZjigDtlJj9' },
+// ]).then(() => {
+// this.setState({ done: true })
+// }).catch((e) => {
+// console.warn(e)
+// this.setState({ done: true })
+// })
+ }
+ reset() {
+ this.setState({
+ name: '',
+ email: '',
+ message: '',
+ track: false,
+ publish: false,
+ sending: false,
+ done: false,
+ })
+ }
+ render() {
+ return (
+ <ScrollableContainer heading='Contact' style={styles.container} bodyStyle={styles.body}>
+ <View style={styles.innerContainer}>
+ <ClearText style={styles.bodyText}>
+ {this.props.content.body}
+ </ClearText>
+
+ {this.renderForm()}
+ {this.renderComments()}
+ </View>
+
+ <Modal visible={this.state.sending}>
+ {this.renderActivity()}
+ </Modal>
+ </ScrollableContainer>
+ )
+ }
+ renderForm() {
+ let error = this.state.error ? (
+ <ClearText>Please enter a valid email address</ClearText>
+ ) : (
+ <View></View>
+ )
+ return (
+ <View style={styles.keyboardAvoidingViewInner}>
+ <TextInput
+ value={this.state.name}
+ placeholder="Name"
+ autoCorrect={false}
+ onChangeText={this.setName}
+ style={styles.textInput} />
+ <TextInput
+ value={this.state.email}
+ placeholder="Email"
+ autoCapitalize='none'
+ autoCorrect={false}
+ keyboardType='email-address'
+ onChangeText={this.setEmail}
+ style={styles.textInput} />
+ {error}
+ <TextInput
+ multiline={true}
+ placeholder="Enter your message"
+ value={this.state.message}
+ onChangeText={this.setMessage}
+ style={[styles.textInput, styles.textArea]} />
+ <View style={styles.checkboxes}>
+ <CheckBox
+ label='Sign me up for emails from the Armory'
+ containerStyle={styles.checkboxContainerStyle}
+ labelStyle={styles.checkboxLabelStyle}
+ checked={this.state.track}
+ onChange={this.setTrack} />
+ <CheckBox
+ label='Publish my comment for others to read'
+ containerStyle={styles.checkboxContainerStyle}
+ labelStyle={styles.checkboxLabelStyle}
+ checked={this.state.publish}
+ onChange={this.setPublish} />
+ </View>
+ <Button label='SEND' onPress={this.send} />
+ </View>
+ )
+ }
+ renderActivity() {
+ let activity;
+ if (! this.state.done) {
+ activity = (
+ <View style={styles.message}>
+ <ActivityIndicator animating={true} color='#fff' />
+ <Heading>Sending your message...</Heading>
+ </View>
+ )
+ }
+ else {
+ activity = (
+ <View style={styles.message}>
+ <Heading>Thanks!</Heading>
+ <ClearText>Your message has been delivered.</ClearText>
+ <ClearText style={styles.lastMessage}>Thank you for your feedback.</ClearText>
+ <Button onPress={this.reset} label='OK' />
+ </View>
+ )
+ }
+ return (
+ <View style={styles.modal}>
+ {activity}
+ </View>
+ )
+ }
+ renderComments() {
+ const comments = this.props.comments.map( (comment, i) => {
+ const date = comment[0].replace(/:\d\d$/,'')
+ return (
+ <View style={styles.comment} key={'comment_' + i}>
+ <View style={styles.commentRow}>
+ <ClearText style={styles.commentName}>{comment[1]}</ClearText>
+ <ClearText style={styles.commentDate}>{date}</ClearText>
+ </View>
+ <ClearText style={styles.commentBody}>{comment[2]}</ClearText>
+ </View>
+ )
+ })
+ return (
+ <TouchableWithoutFeedback>
+ <View style={styles.comments}>
+ {comments}
+ </View>
+ </TouchableWithoutFeedback>
+ )
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ flex: 1,
+ },
+ body: {
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ },
+ innerContainer: {
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ },
+ bodyText: {
+ width: 400,
+ marginBottom: 20,
+ },
+ message: {
+ backgroundColor: 'rgb(0,0,0)',
+ borderRadius: 20,
+ padding: 20,
+ },
+ modal: {
+ backgroundColor: 'rgba(0,0,0,0.9)',
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ textInput: {
+ width: 350,
+ height: 40,
+ padding: 5,
+ fontSize: 15,
+ fontFamily: 'Futura-Medium',
+ backgroundColor: 'white',
+ borderColor: 'gray',
+ borderWidth: 1,
+ marginTop: 10,
+ marginLeft: 5,
+ marginBottom: 5,
+ marginRight: 5,
+ },
+ textArea: {
+ width: 350,
+ height: 200,
+ },
+ checkboxes: {
+ alignItems: 'flex-start',
+ marginLeft: 5,
+ },
+ checkboxContainerStyle: {
+ marginTop: 10,
+ },
+ checkboxLabelStyle: {
+ color: '#ddd',
+ fontFamily: 'Futura-Medium',
+ },
+ keyboardAvoidingViewInner: {
+ alignItems: 'center',
+ },
+ lastMessage: {
+ marginBottom: 10,
+ },
+
+ comments: {
+ marginTop: 20,
+ paddingBottom: 100,
+ width: 400,
+ },
+ comment: {
+ borderWidth: 1,
+ borderColor: 'rgb(128,128,128)',
+ padding: 10,
+ margin: 10,
+ },
+ commentRow: {
+ flexDirection: 'row',
+ },
+ commentName: {
+ flex: 1,
+ fontSize: 13,
+ textAlign: 'left',
+ },
+ commentDate: {
+ flex: 1,
+ fontSize: 13,
+ textAlign: 'right',
+ },
+ commentBody: {
+ marginTop: 10,
+ fontSize: 13,
+ borderTopWidth: 1,
+ borderTopColor: 'rgb(64,64,64)',
+ textAlign: 'left',
+ },
+})
+
diff --git a/client/src/lib/views/credits.js b/client/src/lib/views/credits.js
new file mode 100644
index 0000000..dbe516b
--- /dev/null
+++ b/client/src/lib/views/credits.js
@@ -0,0 +1,34 @@
+import React, { Component } from 'react';
+import {
+ StyleSheet,
+ View,
+} from 'react-native';
+import HTMLView from 'react-native-htmlview'
+
+import htmlStyles from '../components/htmlStyles'
+import ScrollableContainer from '../components/scrollableContainer'
+import ClearText from '../components/text'
+
+export default class Credits extends Component {
+ constructor(props) {
+ super()
+ }
+ render() {
+ let body = '<p>' + this.props.content.body + '</p>'
+ return (
+ <ScrollableContainer heading='Credits'>
+ <View style={styles.body}>
+ <View style={styles.inner}>
+ <HTMLView value={body} stylesheet={htmlStyles} onLinkPress={this.props.onLinkPress} />
+ </View>
+ </View>
+ </ScrollableContainer>
+ )
+ }
+}
+
+const styles = StyleSheet.create({
+ body: {
+ maxWidth: '650px',
+ },
+})
diff --git a/client/src/lib/views/home.js b/client/src/lib/views/home.js
new file mode 100644
index 0000000..618ce64
--- /dev/null
+++ b/client/src/lib/views/home.js
@@ -0,0 +1,98 @@
+import React, { Component } from 'react';
+import {
+ Image,
+ StyleSheet,
+ TouchableOpacity,
+ View,
+ Platform
+} from 'react-native';
+import { Link } from 'react-router-dom'
+import ClearText from '../components/text'
+import FadeInView from 'react-native-fade-in-view'
+
+const sections = [
+ '/timeline',
+ '/drones',
+ '/livestream',
+ '/information',
+ '/contact'
+]
+const labels = [
+ 'SURVEILLANCE',
+ 'MILITARY DRONE STATISTICS',
+ 'LIVESTREAM',
+ 'ABOUT THIS WORK',
+ 'CONTACT',
+]
+const images = [
+ require('../../img/nav/home/timeline.png'),
+ require('../../img/nav/home/drones.png'),
+ require('../../img/nav/home/livestream.png'),
+ require('../../img/nav/home/information.png'),
+ require('../../img/nav/home/contact.png'),
+]
+
+export default class Home extends Component {
+ constructor() {
+ super()
+ }
+ render() {
+ const links = sections.map((path, i) => {
+ const image = images[i]
+ const label = labels[i]
+ return view(path, image, label)
+ })
+ return (
+ <FadeInView duration={500} style={styles.container}>
+ {links}
+ </FadeInView>
+ )
+ }
+}
+
+function view (path, image, label, isSelected) {
+ let key = ('home_' + path).replace('/','')
+ let imageKey = 'image_' + key + String(isSelected)
+ return (
+ <Link to={path} key={key}>
+ <View style={styles.navItem}>
+ <Image key={imageKey} style={styles.image} source={image} />
+ <ClearText style={styles.text}>{label}</ClearText>
+ </View>
+ </Link>
+ )
+}
+
+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 shortHeight = isIphone || window.innerHeight < 700
+
+const buttonSide = shortHeight ? 50 : 70
+const buttonMargin = shortHeight ? 5 : 10
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginTop: -80
+ },
+ navItem: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ minHeight: shortHeight ? '10vh' : '13vh',
+ },
+ text: {
+ lineHeight: 16,
+ marginBottom: shortHeight ? 5 : 10,
+ },
+ image: {
+ width: buttonSide,
+ height: buttonSide,
+ margin: buttonMargin,
+ }
+})
diff --git a/client/src/lib/views/information.js b/client/src/lib/views/information.js
new file mode 100644
index 0000000..c48eb7f
--- /dev/null
+++ b/client/src/lib/views/information.js
@@ -0,0 +1,114 @@
+import React, { Component } from 'react';
+import {
+ StyleSheet,
+ Image,
+} from 'react-native';
+
+import ScrollableContainer from '../components/scrollableContainer'
+import ClearText from '../components/text'
+
+export default class Information extends Component {
+ constructor(props) {
+ super()
+ }
+ render() {
+ const content = this.props.content
+ return (
+ <ScrollableContainer heading="ABOUT THIS WORK" bodyStyle={styles.bodyStyle}>
+ <ClearText style={styles.body}>
+ {this.props.content.show}
+ </ClearText>
+
+ <Image source={require('../../img/aiweiwei.png')} resizeMode='cover' style={styles.face} />
+ <ClearText style={styles.body}>
+ {this.props.content.aiWeiWeiBio}
+ </ClearText>
+ <Image source={require('../../img/herzogDeMeuron.png')} resizeMode='cover' style={styles.face} />
+ <ClearText style={styles.body}>
+ {this.props.content.herzogBio}
+ </ClearText>
+ <ClearText style={styles.body}>
+ {this.props.content.deMeuronBio}
+ </ClearText>
+ <ClearText style={styles.footer}>
+ </ClearText>
+ </ScrollableContainer>
+ );
+ }
+}
+
+/*
+ <YouTube
+ ref={(ref) => this.youtubePlayer = ref}
+ videoId={content.video.token}
+ play={false}
+ hidden={false}
+ fullscreen={false}
+ loop={true}
+ controls={1}
+ showFullscreenButton={true}
+ showinfo={false}
+ modestbranding={true}
+ rel={false}
+ iv_load_policy={3}
+
+ onReady={(e)=>{this.setState({isReady: true})}}
+ onChangeState={(e)=>{
+ if (e.state == 'playing') {
+ this.props.onVideoPlay()
+ }
+ else {
+ this.props.onVideoPause()
+ }
+ }}
+ onChangeFullscreen={(e)=>{
+ if (e.isFullscreen) {
+ this.props.onVideoPlay()
+ }
+ else {
+ this.props.onVideoPause()
+ }
+ }}
+ onError={(e)=>{this.setState({error: e.error})}}
+ onProgress={(e)=>{this.setState({currentTime: e.currentTime, duration: Math.round(e.duration)})}}
+
+ style={styles.video}
+ />
+*/
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'flex-start',
+ alignItems: 'flex-start',
+ },
+ bodyStyle: {
+ marginRight: 10,
+ },
+ video: {
+ alignSelf: 'stretch',
+ height: 400,
+ width: '100%',
+ backgroundColor: 'black',
+ marginVertical: 10
+ },
+ body: {
+ textAlign: 'left',
+ paddingRight: 10,
+ width: '90%',
+ marginBottom: 20,
+ maxWidth: 600,
+ },
+ hero: {
+ width: '90%',
+ height: 300,
+ },
+ face: {
+ width: '90vw',
+ height: 400,
+ marginBottom: 20,
+ },
+ footer: {
+ marginBottom: 80,
+ },
+})
diff --git a/client/src/lib/views/livestream.js b/client/src/lib/views/livestream.js
new file mode 100644
index 0000000..d3df460
--- /dev/null
+++ b/client/src/lib/views/livestream.js
@@ -0,0 +1,134 @@
+import React, { Component } from 'react';
+import {
+ StyleSheet,
+ View
+} from 'react-native';
+
+import Container from '../components/container'
+import ClearText from '../components/text'
+import Button from '../components/button'
+import Youtube from '../components/youtube'
+
+export default class Livestream extends Component {
+ constructor(props) {
+ super()
+ this.state = {
+ ytid: getYTID(choice(props.content.streams).uri),
+ isReady: false,
+ currentTime: 0,
+ duration: 0,
+ quality: 0,
+ status: 'unloaded',
+ error: null,
+ }
+ }
+ render() {
+ const buttons = this.props.content.streams.map( (stream, i) => {
+ const ytid = getYTID(stream.uri || "")
+ if (! ytid) return null
+ let buttonStyle, textStyle
+ if (ytid === this.state.ytid) {
+ buttonStyle = styles.activeButton
+ textStyle = styles.activeButtonText
+ }
+ return (
+ <Button
+ key={'button_' + i}
+ buttonStyle={buttonStyle}
+ textStyle={textStyle}
+ label={stream.text}
+ onPress={() => {
+ this.setState({ ytid })
+ }}
+ />
+ )
+ }).filter(button => !!button)
+
+ return (
+ <Container heading='Livestream' bodyStyle={styles.bodyStyle}>
+ <View style={styles.videoContainer} className='videoContainer'>
+ <Youtube ytid={this.state.ytid} />
+ <View style={styles.overlay}></View>
+ </View>
+ <View style={styles.buttons}>
+ {buttons}
+ </View>
+ <View style={styles.contentContainer}>
+ <ClearText style={styles.body}>{this.props.content.body}</ClearText>
+ </View>
+ </Container>
+ )
+ }
+}
+
+function randint(n){ return Math.floor(Math.random() * n) }
+function choice(a){ return a[randint(a.length)] }
+function getYTID(url) {
+ return (
+ url.match(/v=([-_a-zA-Z0-9]{11})/i)
+ || url.match(/youtu.be\/([-_a-zA-Z0-9]{11})/i)
+ || url.match(/embed\/([-_a-zA-Z0-9]{11})/i)
+ )[1].split('&')[0];
+
+}
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ },
+ bodyStyle: {
+ flex: 1,
+ },
+ body: {
+ textAlign: 'left',
+ marginTop: 40,
+ width: '75%',
+ },
+ contentContainer: {
+ flex: 1,
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ width: '100%',
+ },
+ videoContainer: {
+ justifyContent: 'flex-start',
+ height: 400,
+ width: '100%',
+ padding: 10,
+ },
+ video: {
+ alignSelf: 'stretch',
+ height: 400,
+ width: '100%',
+ backgroundColor: 'black',
+ marginVertical: 10
+ },
+ overlay: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ width: '100%',
+ height: '100%',
+ backgroundColor: 'rgba(0,0,0,0)'
+ },
+ buttons: {
+ width: '100%',
+ paddingTop: 20,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ button: {
+ margin: 20,
+ },
+ activeButtonText: {
+ color: 'red',
+ },
+ activeButton: {
+ backgroundColor: '#bbb',
+ borderBottomColor: '#888',
+ },
+})
diff --git a/client/src/lib/views/nav.js b/client/src/lib/views/nav.js
new file mode 100644
index 0000000..83f9aa4
--- /dev/null
+++ b/client/src/lib/views/nav.js
@@ -0,0 +1,119 @@
+import React, { Component } from 'react';
+import {
+ Image,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ Platform,
+ View
+} from 'react-native';
+import { Link } from 'react-router-dom'
+import FadeInView from 'react-native-fade-in-view'
+
+const sections = [
+ '/',
+ '/timeline',
+ '/drones',
+ '/livestream',
+ '/information',
+ '/contact'
+]
+const images = [
+ require('../../img/nav/sidebar/home.png'),
+ require('../../img/nav/sidebar/timeline.png'),
+ require('../../img/nav/sidebar/drones.png'),
+ require('../../img/nav/sidebar/livestream.png'),
+ require('../../img/nav/sidebar/information.png'),
+ require('../../img/nav/sidebar/contact.png'),
+]
+
+export default class Nav extends Component {
+ constructor() {
+ super()
+ }
+ render() {
+ let opacity = 1
+ if (this.props.location.pathname === '/' || this.props.location.pathname === '/home') {
+ opacity = 0
+ }
+ const min = this.props.min || 0
+ const max = this.props.max || 6
+ const align = this.props.min == 0 ? styles.flexEnd : styles.flexStart
+ const links = sections.map((path, i) => {
+ const isSelected = this.props.location.pathname === path
+ const image = images[i]
+ return view(path, image, isSelected, align)
+ }).filter( (a,i) => { return min <= i && i < max })
+ return (
+ <View style={[styles.nav, align, {opacity: opacity}]}>
+ {links}
+ </View>
+ )
+ }
+}
+
+function view (path, image, isSelected, align) {
+ const key = 'link_' + (path.replace('/','') || 'home')
+ const style = [styles.image]
+ if (! isSelected) {
+ style.push(styles.unselectedNavItem)
+ }
+ else {
+ style.push(styles.selectedNavItem)
+ }
+ return (
+ <View style={[styles.item, align]} key={key}>
+ <Link to={path}>
+ <View style={[styles.item, align]}>
+ <Image key={'image_' + key} style={style} source={image} />
+ </View>
+ </Link>
+ </View>
+ )
+}
+
+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 styles = StyleSheet.create({
+ nav: {
+ width: '100%',
+ height: 35,
+ flex: 10,
+ flexGrow: 2,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'flex-end',
+ },
+ flexEnd: {
+ alignItems: 'flex-end',
+ paddingRight: isMobile ? 5 : 10,
+ },
+ flexStart: {
+ alignItems: 'flex-start',
+ paddingLeft: isMobile ? 5 : 10,
+ },
+ unselectedNavItem: {
+ opacity: 0.5,
+ },
+ selectedNavItem: {
+ opacity: 1.0,
+ },
+ item: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'flex-end',
+ },
+ image: {
+ margin: "0 auto",
+ justifyContent: 'center',
+ alignItems: 'flex-end',
+ width: 35,
+ height: 35,
+ },
+})
+
+
diff --git a/client/src/lib/views/privacy.js b/client/src/lib/views/privacy.js
new file mode 100644
index 0000000..c2d0596
--- /dev/null
+++ b/client/src/lib/views/privacy.js
@@ -0,0 +1,33 @@
+import React, { Component } from 'react';
+import {
+ StyleSheet,
+ View,
+} from 'react-native';
+import HTMLView from 'react-native-htmlview'
+
+import htmlStyles from '../components/htmlStyles'
+import ScrollableContainer from '../components/scrollableContainer'
+import ClearText from '../components/text'
+
+export default class Privacy extends Component {
+ constructor(props) {
+ super()
+ }
+ render() {
+ let body = '<p>' + this.props.content.body + '</p>'
+ return (
+ <ScrollableContainer heading='Privacy Policy'>
+ <View style={styles.body}>
+ <HTMLView value={body} stylesheet={htmlStyles} onLinkPress={this.props.onLinkPress} />
+ </View>
+ </ScrollableContainer>
+ )
+ }
+}
+
+const styles = StyleSheet.create({
+ body: {
+ width: '90%',
+ maxWidth: '650px',
+ },
+}) \ No newline at end of file