├── .gitignore ├── README.md ├── app ├── .babelrc ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .watchmanconfig ├── App.js ├── app.json ├── assets │ ├── back.png │ ├── icon.png │ └── splash.png ├── package.json ├── src │ ├── AnimUtils.js │ ├── components │ │ ├── CalendarColumns.js │ │ ├── CalendarLayout │ │ │ ├── Sidebar.js │ │ │ └── index.js │ │ ├── EventItems │ │ │ ├── Item.js │ │ │ ├── Item.styles.js │ │ │ └── index.js │ │ └── misc │ │ │ ├── BackButton.js │ │ │ ├── Container.js │ │ │ ├── Header.js │ │ │ └── index.js │ ├── data │ │ ├── events.js │ │ ├── index.js │ │ └── speakers.js │ ├── theme.js │ └── views │ │ ├── Details │ │ ├── index.js │ │ └── styles.js │ │ ├── Home.js │ │ └── Navigator.js ├── vendor │ └── react-native-reanimated-v1.0.0-alpha.3.patchexpo29.2.tgz └── yarn.lock ├── netlify.toml ├── package.json ├── presentation ├── components │ ├── Provider.js │ └── Video.js ├── snippets │ ├── animated.js │ ├── fluidnavigator.js │ ├── gesturehandler.js │ └── panresponder.js └── theme.js ├── workshop.mdx └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # OSX 3 | # 4 | .DS_Store 5 | 6 | # node.js 7 | # 8 | node_modules/ 9 | npm-debug.log 10 | yarn-error.log 11 | 12 | 13 | # Xcode 14 | # 15 | build/ 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | xcuserdata 25 | *.xccheckout 26 | *.moved-aside 27 | DerivedData 28 | *.hmap 29 | *.ipa 30 | *.xcuserstate 31 | project.xcworkspace 32 | 33 | 34 | # Android/IntelliJ 35 | # 36 | build/ 37 | .idea 38 | .gradle 39 | local.properties 40 | *.iml 41 | 42 | # BUCK 43 | buck-out/ 44 | \.buckd/ 45 | *.keystore 46 | 47 | .build -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Animations & Interactions 2 | 3 | ## React Native EU 2018 4 | 5 | In this workshop, we cover: 6 | 7 | - [react-native-reanimated](https://github.com/kmagiera/react-native-reanimated) 8 | - [react-native-gesture-handler](https://github.com/kmagiera/react-native-gesture-handler) ([docsite](https://kmagiera.github.io/react-native-gesture-handler)) 9 | - [FluidTransitions](https://github.com/fram-x/FluidTransitions) 10 | 11 | ## Workshop materials 12 | 13 | https://github.com/FormidableLabs/react-native-interactions-workshop 14 | 15 | ## Presentation 16 | 17 | https://rneu-interactions.netlify.com 18 | 19 | ## Getting started 20 | 21 | ```sh 22 | cd app 23 | yarn 24 | yarn start 25 | ``` 26 | -------------------------------------------------------------------------------- /app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-expo"], 3 | "env": { 4 | "development": { 5 | "plugins": ["transform-react-jsx-source"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - formidable/configurations/es6-react 4 | - prettier 5 | plugins: 6 | - prettier 7 | env: 8 | browser: true 9 | rules: 10 | quotes: 11 | - warn 12 | - single 13 | no-unsafe-negation: 14 | - off 15 | max-nested-callbacks: 16 | - off 17 | arrow-parens: 18 | - off 19 | filenames/match-regex: 20 | - off 21 | no-magic-numbers: 22 | - off 23 | complexity: 24 | - off 25 | max-statements: 26 | - off 27 | no-use-before-define: 28 | - off 29 | valid-jsdoc: 30 | - off 31 | react/prop-types: 32 | - off 33 | react/no-multi-comp: 34 | - off 35 | react/sort-comp: 36 | - off 37 | react/jsx-handler-names: 38 | - off -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | -------------------------------------------------------------------------------- /app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "flow", 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /app/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ThemeProvider } from 'styled-components/native'; 3 | 4 | import Navigator from './src/views/Navigator'; 5 | import * as theme from './src/theme'; 6 | 7 | export default () => ( 8 | 9 | 10 | 11 | ); 12 | 13 | console.ignoredYellowBox = [ 14 | 'Warning: Failed prop type: Invalid prop `onHandlerStateChange`' 15 | ]; 16 | -------------------------------------------------------------------------------- /app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "RN Interactions App", 4 | "description": "RN Interactions Workshop Demo App", 5 | "slug": "rn-interactions-app", 6 | "privacy": "public", 7 | "sdkVersion": "29.0.0", 8 | "platforms": ["ios", "android"], 9 | "version": "1.0.0", 10 | "orientation": "portrait", 11 | "icon": "./assets/icon.png", 12 | "splash": { 13 | "image": "./assets/splash.png", 14 | "resizeMode": "contain", 15 | "backgroundColor": "#ffffff" 16 | }, 17 | "updates": { 18 | "fallbackToCacheTimeout": 0 19 | }, 20 | "assetBundlePatterns": [ 21 | "**/*" 22 | ], 23 | "ios": { 24 | "supportsTablet": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/assets/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-native-interactions-workshop/aaed09c882669fd37496b8795ecc3fe66be9f036/app/assets/back.png -------------------------------------------------------------------------------- /app/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-native-interactions-workshop/aaed09c882669fd37496b8795ecc3fe66be9f036/app/assets/icon.png -------------------------------------------------------------------------------- /app/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-native-interactions-workshop/aaed09c882669fd37496b8795ecc3fe66be9f036/app/assets/splash.png -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rn-interactions-app", 3 | "main": "node_modules/expo/AppEntry.js", 4 | "private": true, 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "eject": "expo eject", 10 | "lint": "eslint src" 11 | }, 12 | "babel": { 13 | "presets": [ 14 | "babel-preset-react-native" 15 | ], 16 | "plugins": [ 17 | "@babel/plugin-proposal-pipeline-operator" 18 | ] 19 | }, 20 | "resolutions": { 21 | "react-native-reanimated": "./vendor/react-native-reanimated-v1.0.0-alpha.3.patchexpo29.2.tgz" 22 | }, 23 | "dependencies": { 24 | "add": "^2.0.6", 25 | "date-fns": "^2.0.0-alpha.16", 26 | "expo": "^29.0.0", 27 | "react": "16.3.1", 28 | "react-native": "https://github.com/expo/react-native/archive/sdk-29.0.0.tar.gz", 29 | "react-native-gesture-handler": "1.0.6", 30 | "react-native-reanimated": "./vendor/react-native-reanimated-v1.0.0-alpha.3.patchexpo29.2.tgz", 31 | "react-navigation": "^2.12.1", 32 | "react-navigation-fluid-transitions": "^0.2.4", 33 | "slugify": "^1.3.1", 34 | "styled-components": "^3.4.5", 35 | "yarn": "^1.9.4" 36 | }, 37 | "devDependencies": { 38 | "@babel/plugin-proposal-pipeline-operator": "^7.0.0-rc.3", 39 | "babel-eslint": "^9.0.0", 40 | "eslint": "^5.5.0", 41 | "eslint-config-formidable": "^4.0.0", 42 | "eslint-config-prettier": "^3.0.1", 43 | "eslint-plugin-filenames": "^1.3.2", 44 | "eslint-plugin-import": "^2.14.0", 45 | "eslint-plugin-prettier": "^2.6.2", 46 | "eslint-plugin-promise": "^4.0.0", 47 | "eslint-plugin-react": "^7.11.1", 48 | "prettier": "^1.14.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/AnimUtils.js: -------------------------------------------------------------------------------- 1 | import Animated from 'react-native-reanimated'; 2 | 3 | // Limit a value to minimum and maximum bounds 4 | export const limit = (a, min, max) => Animated.min(max, Animated.max(min, a)); 5 | 6 | // Run a custom spring animation 7 | export const runSpring = (clock, value, velocity) => { 8 | const state = { 9 | finished: new Animated.Value(0), 10 | position: new Animated.Value(0), 11 | velocity: new Animated.Value(0), 12 | time: new Animated.Value(0), 13 | }; 14 | 15 | const config = { 16 | // When velocity is positive, zoom to snap point `1`, otheriwse 17 | // target snap point `0` 18 | toValue: Animated.cond( 19 | Animated.greaterThan(velocity, 0), 20 | new Animated.Value(1), 21 | new Animated.Value(0) 22 | ), 23 | // Some spring behaviour config: 24 | damping: 7.5, 25 | mass: 1.2, 26 | stiffness: 121.6, 27 | overshootClamping: true, 28 | restSpeedThreshold: 0.001, 29 | restDisplacementThreshold: 0.001, 30 | }; 31 | 32 | return Animated.block([ 33 | // Reset state when we're starting the clock 34 | Animated.cond( 35 | Animated.not(Animated.clockRunning(clock)), [ 36 | Animated.set(state.finished, 0), 37 | Animated.set(state.velocity, velocity), 38 | Animated.set(state.position, value), 39 | Animated.set(state.time, 0), 40 | Animated.startClock(clock), 41 | ] 42 | ), 43 | // Run the animation and stop the clock when we're done 44 | Animated.spring(clock, state, config), 45 | Animated.cond(state.finished, Animated.stopClock(clock)), 46 | state.position 47 | ]); 48 | }; 49 | -------------------------------------------------------------------------------- /app/src/components/CalendarColumns.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { View } from 'react-native'; 3 | // import { PinchGestureHandler, State } from 'react-native-gesture-handler'; 4 | // import Animated from 'react-native-reanimated'; 5 | 6 | // import * as AnimUtils from '../AnimUtils'; 7 | import * as theme from '../theme'; 8 | 9 | const { 10 | CELL_NUM, 11 | CELL_WIDTH, 12 | // CONTAINER_WIDTH, 13 | // PINCH_MAGNITUDE 14 | } = theme.calendar; 15 | 16 | const getColumnWidths = () => { 17 | const arr = Array.from({ length: CELL_NUM }); 18 | return arr.map(() => ({})); 19 | }; 20 | 21 | class CalendarColumns extends Component { 22 | constructor(props) { 23 | super(props); 24 | 25 | // The current column that is being zoomed (none is -1) 26 | const indexState = props.indexState || new Animated.Value(-1); 27 | // The current zoom state, where 0 is closed and 1 is opened 28 | const zoomState = props.zoomState || new Animated.Value(0); 29 | 30 | this.containerStyle = {}; 31 | this.columnStyles = getColumnWidths(); 32 | } 33 | 34 | render() { 35 | const { /*gestureHandlerRef,*/ children } = this.props; 36 | 37 | return ( 38 | 39 | {this.columnStyles.map((style, index) => ( 40 | 41 | {children ? children(index) : null} 42 | 43 | ))} 44 | 45 | ); 46 | } 47 | } 48 | 49 | const styles = { 50 | container: { 51 | flexGrow: 1, 52 | flexShrink: 0, 53 | flexDirection: 'row', 54 | alignItems: 'stretch', 55 | overflow: 'hidden' 56 | }, 57 | column: { 58 | flexGrow: 1, 59 | flexShrink: 0, 60 | flexDirection: 'column', 61 | alignItems: 'stretch', 62 | paddingLeft: 2, 63 | paddingRight: 2, 64 | borderRightWidth: theme.sizes.hairline, 65 | borderRightColor: theme.colors.stroke, 66 | width: CELL_WIDTH 67 | } 68 | }; 69 | 70 | export default CalendarColumns; 71 | -------------------------------------------------------------------------------- /app/src/components/CalendarLayout/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/native'; 3 | import { format, setHours, startOfDay } from 'date-fns'; 4 | 5 | import * as theme from '../../theme'; 6 | const { HOUR_HEIGHT, SIDEBAR_OFFSET } = theme.calendar; 7 | 8 | const Wrapper = styled.View` 9 | flex-direction: column; 10 | align-items: stretch; 11 | top: ${SIDEBAR_OFFSET}px; 12 | `; 13 | 14 | const Row = styled.View` 15 | flex-direction: column; 16 | align-items: flex-start; 17 | height: ${HOUR_HEIGHT}px; 18 | `; 19 | 20 | const Label = styled.Text` 21 | color: ${p => p.theme.colors.label}; 22 | line-height: 12px; 23 | font-size: 10px; 24 | padding: 2px 4px; 25 | `; 26 | 27 | const LABEL_FORMAT = 'HH:mm'; 28 | const start = startOfDay(new Date()); 29 | 30 | const Sidebar = () => ( 31 | 32 | {Array.from({ length: 24 }).map((_, i) => { 33 | const label = format(setHours(start, i), LABEL_FORMAT); 34 | 35 | return {i > 0 && }; 36 | })} 37 | 38 | ); 39 | 40 | export default Sidebar; 41 | -------------------------------------------------------------------------------- /app/src/components/CalendarLayout/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import { View, Platform } from 'react-native'; 3 | import { ScrollView } from 'react-native-gesture-handler'; 4 | import Animated from 'react-native-reanimated'; 5 | import styled from 'styled-components/native'; 6 | 7 | import * as theme from '../../theme'; 8 | import Sidebar from './Sidebar'; 9 | 10 | const { HOUR_HEIGHT, SIDEBAR_OFFSET } = theme.calendar; 11 | 12 | const Scrollable = styled(ScrollView).attrs({ 13 | bounces: false, 14 | contentContainerStyle: { 15 | flexDirection: 'row', 16 | alignItems: 'stretch' 17 | } 18 | })` 19 | flex-grow: 1; 20 | flex-shrink: 0; 21 | width: 100%; 22 | `; 23 | 24 | const Content = styled.View.attrs({ 25 | collapsable: false 26 | })` 27 | width: ${p => p.theme.calendar.CONTAINER_WIDTH}; 28 | flex-direction: row; 29 | align-items: stretch; 30 | `; 31 | 32 | const getBackgroundStyle = zoom => { 33 | const opacity = Animated.interpolate(zoom, { 34 | inputRange: [0, 0.5, 1], 35 | outputRange: [0, 0, 1] 36 | }); 37 | 38 | const scaleX = Animated.interpolate(zoom, { 39 | inputRange: [0, 0.5, 1], 40 | outputRange: [0.8, 0.8, 1] 41 | }); 42 | 43 | return { 44 | opacity, 45 | transform: [{ scaleX }] 46 | }; 47 | }; 48 | 49 | const getBorderStyle = zoom => { 50 | const alpha = Animated.interpolate(zoom, { 51 | inputRange: [0, 0.5, 1], 52 | outputRange: [1, 1, 0] 53 | }); 54 | 55 | const { strokeRgb } = theme.colors; 56 | const borderRightColor = Animated.color( 57 | strokeRgb[0], 58 | strokeRgb[1], 59 | strokeRgb[2], 60 | alpha 61 | ); 62 | 63 | return { borderRightColor }; 64 | }; 65 | 66 | class CalendarLayout extends Component { 67 | constructor(props) { 68 | super(props); 69 | 70 | const zoomState = props.zoomState || new Animated.Value(0); 71 | 72 | this.bgStyle = getBackgroundStyle(zoomState); 73 | this.borderStyle = getBorderStyle(zoomState); 74 | this.waitFor = createRef(); 75 | this.scrollView = createRef(); 76 | } 77 | 78 | render() { 79 | const { waitFor } = this; 80 | const { children, initialScrollY } = this.props; 81 | const renderProps = { gestureHandlerRef: waitFor }; 82 | const scrollPosition = { x: 0, y: initialScrollY + SIDEBAR_OFFSET }; 83 | 84 | return ( 85 | { 90 | // contentOffset not supported on Android 91 | if (Platform.OS === 'android' && this.scrollView.current) { 92 | this.scrollView.current.scrollTo(scrollPosition); 93 | } 94 | }} 95 | > 96 | 97 | 98 | 99 | 100 | 101 | 102 | {backgroundRows} 103 | 104 | 105 | {children(renderProps)} 106 | 107 | 108 | ); 109 | } 110 | } 111 | 112 | const styles = { 113 | background: { 114 | position: 'absolute', 115 | top: 0, 116 | left: 0, 117 | bottom: 0, 118 | right: 0, 119 | height: HOUR_HEIGHT * 24, 120 | flexDirection: 'column', 121 | alignItems: 'stretch' 122 | }, 123 | row: { 124 | height: HOUR_HEIGHT, 125 | borderBottomWidth: theme.sizes.hairline, 126 | borderBottomColor: theme.colors.stroke 127 | }, 128 | sidebar: { 129 | zIndex: 1, 130 | width: theme.calendar.SIDEBAR_WIDTH, 131 | flexDirection: 'row', 132 | alignItems: 'stretch', 133 | backgroundColor: 'white', 134 | borderRightWidth: theme.sizes.hairline, 135 | borderRightColor: theme.colors.stroke 136 | } 137 | }; 138 | 139 | const backgroundRows = Array.from({ length: 23 }).map((_, i) => ( 140 | 141 | )); 142 | 143 | export default CalendarLayout; 144 | -------------------------------------------------------------------------------- /app/src/components/EventItems/Item.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import { Animated } from 'react-native'; 3 | // import { Transition } from 'react-navigation-fluid-transitions'; 4 | 5 | import { 6 | Wrapper, 7 | Touchable, 8 | Card, 9 | Title, 10 | Speaker 11 | } from './Item.styles' 12 | 13 | const CardWithContent = ({ height, item }) => { 14 | return ( 15 | 16 | {item.title} 17 | {item.profile && {item.profile.name}} 18 | 19 | ); 20 | }; 21 | 22 | const Item = ({ item, style, onPress }) => { 23 | const height = style.height || 0; 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default Item; 35 | -------------------------------------------------------------------------------- /app/src/components/EventItems/Item.styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import { Platform } from 'react-native'; 3 | import { RectButton } from 'react-native-gesture-handler'; 4 | 5 | import * as theme from '../../theme'; 6 | 7 | const ellipsizeMode = Platform.select({ 8 | ios: 'clip', 9 | android: 'tail' 10 | }); 11 | 12 | export const Wrapper = styled.View` 13 | flex-direction: column; 14 | align-items: stretch; 15 | border-top-width: 1px; 16 | border-top-color: white; 17 | `; 18 | 19 | export const Touchable = styled(RectButton).attrs({ 20 | underlayColor: 'white', 21 | activeOpacity: 0.5 22 | })` 23 | flex-direction: column; 24 | align-items: stretch; 25 | `; 26 | 27 | export const Card = styled.View` 28 | flex-grow: 1; 29 | flex-shrink: 0; 30 | flex-direction: column; 31 | align-items: stretch; 32 | 33 | background-color: ${p => 34 | p.isTalk ? theme.colors.card : theme.colors.inactive}; 35 | 36 | border-radius: 4px; 37 | padding: 4px; 38 | height: ${p => p.height}px; 39 | `; 40 | 41 | export const Title = styled.Text.attrs({ 42 | allowFontScaling: false, 43 | numberOfLines: p => (p.expand ? 2 : 1), 44 | ellipsizeMode 45 | })` 46 | flex-grow: 0; 47 | flex-shrink: 1; 48 | color: white; 49 | font-size: 12px; 50 | line-height: 15px; 51 | `; 52 | 53 | export const Speaker = styled.Text.attrs({ 54 | allowFontScaling: false, 55 | numberOfLines: 1, 56 | ellipsizeMode 57 | })` 58 | flex-grow: 0; 59 | flex-shrink: 2; 60 | color: white; 61 | font-style: italic; 62 | font-size: 10px; 63 | line-height: 15px; 64 | `; 65 | -------------------------------------------------------------------------------- /app/src/components/EventItems/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import styled from 'styled-components/native'; 3 | import { getHours, getMinutes } from 'date-fns'; 4 | 5 | import * as theme from '../../theme'; 6 | import Item from './Item'; 7 | 8 | const { HOUR_HEIGHT } = theme.calendar; 9 | 10 | const Wrapper = styled.View` 11 | height: ${HOUR_HEIGHT * 24}px; 12 | flex-direction: column; 13 | align-items: stretch; 14 | `; 15 | 16 | class EventItems extends Component { 17 | render() { 18 | const { data, navigate } = this.props; 19 | const { items } = data; 20 | 21 | let y = 0; 22 | 23 | // Calculate a _relative_ top distance from item to item, 24 | // instead of using absolute 25 | const styles = items.map(item => { 26 | const hour = getHours(item.date) + getMinutes(item.date) / 60; 27 | const top = hour * HOUR_HEIGHT; 28 | const height = item.duration * HOUR_HEIGHT; 29 | const marginTop = top - y; 30 | 31 | y = top + height; 32 | return { marginTop, height }; 33 | }); 34 | 35 | return ( 36 | 37 | {items.map((item, i) => ( 38 | { 43 | navigate('Event', { 44 | event: item 45 | }); 46 | }} 47 | /> 48 | ))} 49 | 50 | ); 51 | } 52 | } 53 | 54 | export default EventItems; 55 | -------------------------------------------------------------------------------- /app/src/components/misc/BackButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/native'; 3 | import { BorderlessButton } from 'react-native-gesture-handler'; 4 | 5 | const Button = styled(BorderlessButton)` 6 | position: absolute; 7 | left: 20px; 8 | top: 32px; 9 | `; 10 | 11 | const Arrow = styled.Image.attrs({ 12 | source: require('../../../assets/back.png') 13 | })` 14 | width: 40px; 15 | height: 40px; 16 | padding: 10px; 17 | opacity: 0.8; 18 | `; 19 | 20 | const BackButton = ({ onPress }) => ( 21 | 24 | ); 25 | 26 | export default BackButton; 27 | -------------------------------------------------------------------------------- /app/src/components/misc/Container.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | const Container = styled.View` 4 | background: white; 5 | height: 100%; 6 | flex-direction: column; 7 | align-items: stretch; 8 | `; 9 | 10 | export default Container; 11 | -------------------------------------------------------------------------------- /app/src/components/misc/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Text } from 'react-native'; 3 | import styled from 'styled-components/native'; 4 | import Animated from 'react-native-reanimated'; 5 | import { Constants } from 'expo'; 6 | 7 | import * as theme from '../../theme'; 8 | const { CELL_NUM, CELL_WIDTH } = theme.calendar; 9 | 10 | const Outer = styled.SafeAreaView` 11 | flex-shrink: 0; 12 | background: ${p => p.theme.colors.header}; 13 | border-bottom-width: 1px; 14 | border-bottom-color: ${p => p.theme.colors.stroke}; 15 | padding-top: ${Constants.statusBarHeight}; 16 | `; 17 | 18 | const Inner = styled.View.attrs({ 19 | collapsable: false 20 | })` 21 | height: 80px; 22 | padding-left: ${p => p.theme.calendar.SIDEBAR_WIDTH}px; 23 | 24 | flex-direction: column; 25 | justify-content: flex-end; 26 | align-items: stretch; 27 | `; 28 | 29 | const TitleRow = styled.View.attrs({ 30 | collapsable: false 31 | })` 32 | flex-direction: column; 33 | justify-content: flex-start; 34 | align-items: flex-start; 35 | height: 25px; 36 | `; 37 | 38 | const Title = styled.Text` 39 | text-align: left; 40 | font-size: 18px; 41 | color: ${p => p.theme.colors.text}; 42 | `; 43 | 44 | const Row = styled.View.attrs({ 45 | collapsable: false 46 | })` 47 | flex-direction: row; 48 | align-items: stretch; 49 | `; 50 | 51 | const getFloatingPositions = (index, zoom) => { 52 | const arr = Array.from({ length: CELL_NUM }); 53 | const opacity = Animated.interpolate(zoom, { 54 | inputRange: [0, 0.5, 1], 55 | outputRange: [1, 0, 0] 56 | }); 57 | 58 | return arr.map((_, i) => { 59 | const offsetX = -1 * i * CELL_WIDTH - theme.sizes.mid; 60 | const offsetY = -1 * theme.sizes.mid; 61 | const isZooming = Animated.eq(index, i); 62 | 63 | const translateX = Animated.interpolate(zoom, { 64 | inputRange: [0, 0.8, 1], 65 | outputRange: [0, offsetX, offsetX] 66 | }); 67 | 68 | const translateY = Animated.interpolate(zoom, { 69 | inputRange: [0, 0.8, 1], 70 | outputRange: [0, offsetY, offsetY] 71 | }); 72 | 73 | return { 74 | opacity: Animated.cond(isZooming, 1, opacity), 75 | transform: [ 76 | { translateX: Animated.cond(isZooming, translateX) }, 77 | { translateY: Animated.cond(isZooming, translateY) } 78 | ] 79 | }; 80 | }); 81 | }; 82 | 83 | const getTitleOpacities = (index, zoom) => { 84 | const arr = Array.from({ length: CELL_NUM }); 85 | const opacity = Animated.interpolate(zoom, { 86 | inputRange: [0, 0.1, 0.8, 1], 87 | outputRange: [0, 0, 1, 1] 88 | }); 89 | 90 | return arr.map((_, i) => { 91 | const isZooming = Animated.eq(index, i); 92 | return { opacity: Animated.cond(isZooming, opacity, 0) }; 93 | }); 94 | }; 95 | 96 | class Header extends Component { 97 | constructor(props) { 98 | super(props); 99 | 100 | const indexState = props.indexState || new Animated.Value(-1); 101 | const zoomState = props.zoomState || new Animated.Value(0); 102 | 103 | this.floatingPositions = getFloatingPositions(indexState, zoomState); 104 | this.titleOpacities = getTitleOpacities(indexState, zoomState); 105 | } 106 | 107 | render() { 108 | const { labels, titles } = this.props; 109 | 110 | return ( 111 | 112 | 113 | 114 | React Native EU 115 | 116 | 117 | 118 | {labels.map((label, i) => { 119 | const title = titles[i]; 120 | const floatPos = this.floatingPositions[i]; 121 | const titleOpacity = this.titleOpacities[i]; 122 | 123 | return ( 124 | 125 | {label} 126 | 130 | {title} 131 | 132 | 133 | ); 134 | })} 135 | 136 | 137 | 138 | ); 139 | } 140 | } 141 | 142 | const styles = { 143 | cell: { 144 | flexDirection: 'row', 145 | width: CELL_WIDTH 146 | }, 147 | label: { 148 | textAlign: 'left', 149 | fontWeight: '500', 150 | fontSize: 10, 151 | color: theme.colors.label, 152 | paddingLeft: theme.sizes.mid, 153 | paddingTop: theme.sizes.mid, 154 | paddingBottom: theme.sizes.mid 155 | }, 156 | title: { 157 | flexShrink: 0, 158 | width: 3 * CELL_WIDTH, 159 | paddingLeft: 0 160 | } 161 | }; 162 | 163 | export default Header; 164 | -------------------------------------------------------------------------------- /app/src/components/misc/index.js: -------------------------------------------------------------------------------- 1 | export { default as Container } from './Container' 2 | export { default as Header } from './Header' 3 | export { default as BackButton } from './BackButton' 4 | -------------------------------------------------------------------------------- /app/src/data/events.js: -------------------------------------------------------------------------------- 1 | // scraped from https://react-native.eu/ 2 | /* 3 | copy( 4 | JSON.stringify( 5 | $$("li.agenda__item").map(item => { 6 | const time = item.querySelector("time"); 7 | const speaker = item.querySelector("h4.agenda__header"); 8 | const company = item.querySelector(".agenda__company"); 9 | const title = item.querySelector("p.agenda__header"); 10 | const agenda = item.querySelector("p.agenda__text"); 11 | const date = time ? time.getAttribute("datetime") : null; 12 | 13 | return { 14 | day: date ? new Date(Date.parse(date)).getDate() : null, 15 | time: date 16 | ? new Date(Date.parse(date)).toLocaleTimeString().slice(0, 5) 17 | : null, 18 | date: time ? time.getAttribute("datetime") : null, 19 | speaker: speaker ? speaker.innerText : "", 20 | company: company ? company.innerText : "", 21 | title: title ? title.innerText : "", 22 | agenda: agenda ? agenda.innerHTML : "" 23 | }; 24 | }), 25 | null, 26 | 2 27 | ) 28 | ); 29 | */ 30 | 31 | const data = [ 32 | { 33 | day: 5, 34 | time: '08:30', 35 | date: '2018-09-05 08:30', 36 | speaker: 'Registration', 37 | company: '', 38 | title: '', 39 | agenda: '' 40 | }, 41 | { 42 | day: 5, 43 | time: '09:20', 44 | date: '2018-09-05 09:20', 45 | company: 'CALLSTACK', 46 | title: 'Introduction', 47 | agenda: '', 48 | duration: 0.16 49 | }, 50 | { 51 | day: 5, 52 | time: '09:30', 53 | date: '2018-09-05 09:30', 54 | speaker: 'Mike Grabowski', 55 | company: 'CALLSTACK', 56 | title: 'Keynote', 57 | agenda: '' 58 | }, 59 | { 60 | day: 5, 61 | time: '10:00', 62 | date: '2018-09-05 10:00', 63 | speaker: 'Eric Vicenti', 64 | company: '', 65 | title: 'Owning Transitions', 66 | agenda: 67 | 'The "Transitioner" is the view in React Navigation responsible for animating between screens and supporting navigation gestures. We will discuss how views like "stack" work today, how they evolved, and how they fit into React Navigation and other navigation libraries. Historically, it has been quite difficult to create custom screen transitions and gestures. So the time has come to introduce a new Transitioner, and give you complete control over screen transitions in your React Native App.' 68 | }, 69 | { 70 | day: 5, 71 | time: '10:30', 72 | date: '2018-09-05 10:30', 73 | speaker: 'Coffee break', 74 | company: '', 75 | title: '', 76 | agenda: '' 77 | }, 78 | { 79 | day: 5, 80 | time: '11:00', 81 | date: '2018-09-05 11:00', 82 | speaker: 'Roy Derks', 83 | company: 'HACKTEAM', 84 | title: 'GraphQL Will Do To REST What JSON Did To XML', 85 | agenda: 86 | 'Why GraphQL will become the new standard for accessing external data in your React Native app. I will show how using GraphQL instead of REST services the development process becomes even more declarative as GraphQL will take away the (imperative) hassle of tying data from multiple endpoints together. This will increase the level of complexity in React Native development, while also increasing the performance of the application.' 87 | }, 88 | { 89 | day: 5, 90 | time: '11:30', 91 | date: '2018-09-05 11:30', 92 | speaker: 'Sara Vieira', 93 | company: 'YLD', 94 | title: 'GQL all the Things!', 95 | agenda: 96 | '\n Let\'s take a look at look at what we can do with React Apollo today, how we can use it to even manage our local state and see what the future holds for us in the new releases of React Apollo.\n ' 97 | }, 98 | { 99 | day: 5, 100 | time: '12:00', 101 | date: '2018-09-05 12:00', 102 | speaker: 'Krzysztof Magiera', 103 | company: 'SOFTWARE MANSION', 104 | title: 'It all starts with navigation', 105 | agenda: 106 | '\n For the past 2.5 years I\'ve been working on a bunch of projects, all aiming to expose some native capabilities to be controlled from JavaScript in React Native app. Most notable ones were: native driver support for Animated and two libraries: React Native Gesture Handler and Reanimated. All these projects were inspired by needs of navigation libraries and were meant to fill the gap between platform native navigation solutions and ones that are available in React Native.
\n Our React Native projects often starts by deciding whether to go with native navigation (react-native-navigation) or JS navigation library (react-navigation). But given all these improvements can we still call react-navigation "not native"? In this talk we will learn how Gesture Handler and Reanimated libraries came to live with navigation being the driving force. In this context I will discuss how far is react-navigation from finally proclaiming the badge of really native navigation library.\n ' 107 | }, 108 | { 109 | day: 5, 110 | time: '12:30', 111 | date: '2018-09-05 12:30', 112 | speaker: 'Lunch', 113 | company: '', 114 | title: '', 115 | agenda: '' 116 | }, 117 | { 118 | day: 5, 119 | time: '13:30', 120 | date: '2018-09-05 13:30', 121 | speaker: 'Quinlan Jung', 122 | company: 'EXPO', 123 | title: 'Self hosting mobile applications', 124 | agenda: 125 | 'When building a website, you expose an endpoint where you control the contents that are served back to a user. What if you could do this with a mobile app? Quin will be talking about how you can host your own javascript bundles, writing your own policies so you control exactly what version of the app different users get, and pushing them over the air to users for real time updates.' 126 | }, 127 | { 128 | day: 5, 129 | time: '14:00', 130 | date: '2018-09-05 14:00', 131 | speaker: 'Pavlo Babenko', 132 | company: 'AXSY', 133 | title: 134 | 'Wrangling Your Workflow - Taking Charge of React Native Deployments', 135 | agenda: 136 | 'How to hang a painting on the wall with React Native\n So you want to buy a painting, but not sure which one. Or where to place it? we have a solution. Just use your phone for this.\n Augmented reality becomes more popular in recent years. Companies like Apple or Google invests tons of money to develop AR support. Smartphones became powerful enough to deal with all these new features. And it\'ll be ridiculous not to use all this power when looking for the best place to put a piece of art.\n In this talk we\'ll be looking at ARKit from Apple and the ways how we can use it with React Native. We\'ll be building a simple app to hang paintings on the wall and learning how to deal with newest AR technologies.' 137 | }, 138 | { 139 | day: 5, 140 | time: '14:30', 141 | date: '2018-09-05 14:30', 142 | speaker: 'Radek Pietruszewski', 143 | company: 'NOZBE', 144 | title: 'Next-generation React databases', 145 | agenda: 146 | '\n How do you manage user data in your React Native app? A popular approach is to use Redux or MobX with a persistence adapter. This works great for small, simple apps, but it has a serious flaw: it doesn\'t scale very well.\n Introducing WatermelonDB — a next generation database for React and React Native. Here\'s why you should care:
\n - Highly scalable. Works as well for 20,000 database records as it does for 100.\n Instant launch. No matter how much data you have, the app still launches with near-zero performance penalty
\n - Lazy loaded everything. Don\'t load all data at launch, query only the exact records you need. That\'s why it\'s fast!
\n - Fully reactive. All records and queries are observable using RxJS. A change in one place propagates to all other places automatically
\n - Made for React. Comes with a HOC so you can declaratively tie together data and components
\n - Relational. Document stores are simple, but relational databases are powerful. Want to find, say, all posts that have a comment by user X? Sure, that\'ll be about a millisecond.
\n - Cross-platform. Works on iOS and Android (using SQLite), on the web (using LokiJS), and it\'s easy to add more
\n - Performant. Uses asynchronous processing, multi-threading (also on the web), resource sharing, and caching to squeeze extra performance
\n - Built for sync. Comes with syncing primitives so you can supply your own synchronization code
\n ' 147 | }, 148 | { 149 | day: 5, 150 | time: '15:00', 151 | date: '2018-09-05 15:00', 152 | company: 'CALLSTACK', 153 | title: 'Lightning Talks', 154 | agenda: 155 | 'Five high quality lightning talks by different speakers: "Paper: Welcome to Material Design 2.0", "Understanding "Native" in React Native", "Going Native: Reusing Code", "Building Bluetooth powered React Native Apps", and "Developing iOS apps on Linux"' 156 | }, 157 | { 158 | day: 5, 159 | time: '16:00', 160 | date: '2018-09-05 16:00', 161 | speaker: 'Coffee break', 162 | company: '', 163 | title: '', 164 | agenda: '' 165 | }, 166 | { 167 | day: 5, 168 | time: '16:30', 169 | date: '2018-09-05 16:30', 170 | speaker: 'Matt Hargett', 171 | company: 'PLAYSTATION', 172 | title: '', 173 | agenda: 'Bringing React Native to the next billion devices' 174 | }, 175 | { 176 | day: 5, 177 | time: '17:00', 178 | date: '2018-09-05 17:00', 179 | speaker: 'Stanisław Chmiela', 180 | company: 'SOFTWARE MANSION', 181 | title: 'Chopping Expo up into universal modules to take over the world', 182 | agenda: 183 | 'Expo SDK (part of Expo toolchain) is an open-source library built around React Native, allowing developers to access native components and APIs without too much fuss. Until recently it has been a big monolith, it was hard for developers to either remove unused APIs from their bundle or to use some Expo code in vanilla React Native applications. We’ve decided it is time for Expo to be redesigned and split into multiple, separate native + JS (+ more 🤭) libraries which developers will be able to opt-out of using in Expo standalone apps and also to use in their plain RN projects. This talk will describe the process of making this transition, design of the solution we’ve settled on and difficulties we had to overcome along the way (and how we’ve done it).' 184 | }, 185 | { 186 | day: 5, 187 | time: '17:30', 188 | date: '2018-09-05 17:30', 189 | speaker: 'Evening party', 190 | company: 'FREE FOOD, FREE BEER & GREAT PEOPLE !', 191 | title: '', 192 | agenda: 'Location:' 193 | }, 194 | { 195 | day: 6, 196 | time: '09:30', 197 | date: '2018-09-06 09:30', 198 | speaker: 'Gant Laborde', 199 | company: 'INFINITE RED', 200 | title: 'Taming The Machine', 201 | agenda: 202 | '\n Machine Learning is a buzzword, because big companies love it. Big companies love it, because it\'s insanely cool. Join me as we cover the breath-taking aspects of Machine Learning (ML) and dive deep into how it\'s currently showing up in mobile; especially React Native. We\'ll touch the tip of the iceberg and spark ML creativity for everyone!\n ' 203 | }, 204 | { 205 | day: 6, 206 | time: '10:00', 207 | date: '2018-09-06 10:00', 208 | speaker: 'Jani Eväkallio', 209 | company: 'FORMIDABLE LABS', 210 | title: 'This is a Talk About You', 211 | agenda: 212 | '\n Over the last three years, I’ve spoken to hundreds of React Native developers, and slowly a picture emerges. It’s a picture of excitement and frustration, obsession with technology and pushing the boundaries. It’s a picture of you.\n This talk will blend meticulous research, subjective personal experiences and speculative fiction to discover the core of that elusive Developer Experience, and ask the inconvenient questions you’ve always been afraid to ask. ' 213 | }, 214 | { 215 | day: 6, 216 | time: '10:30', 217 | date: '2018-09-06 10:30', 218 | speaker: 'Rafael de Oleza', 219 | company: 'FACEBOOK', 220 | title: 'Building JavaScript bundles for React Native', 221 | agenda: 222 | '\n React Native uses Metro for building the JavaScript code that runs on the devices, both during development and production.
\n In this talk you\'ll learn the main concepts about bundling JavaScript code (both for web applications and React Native) and the architecture of Metro, which is focused around build speed.\n ' 223 | }, 224 | { 225 | day: 6, 226 | time: '11:00', 227 | date: '2018-09-06 11:00', 228 | speaker: 'Coffee break', 229 | company: '', 230 | title: '', 231 | agenda: '' 232 | }, 233 | { 234 | day: 6, 235 | time: '11:30', 236 | date: '2018-09-06 11:30', 237 | speaker: 'Harry Tormey', 238 | company: '', 239 | title: 'React Native Brownfield - Lessons from the Trenches', 240 | agenda: 241 | '\n Hot reloading--the ability to push over the air updates to published apps and the promise of having your app “just work” out of the box on Android as well as iOS--make React Native a tempting proposition for frontend developers. But what do you do when you have to integrate React Native into a large existing native code base?\n ' 242 | }, 243 | { 244 | day: 6, 245 | time: '12:00', 246 | date: '2018-09-06 12:00', 247 | speaker: 'Samuli Hakoniemi', 248 | company: 'PUNOS MOBILE', 249 | title: 'Better User Experience With Animations', 250 | agenda: 251 | 'Most user interfaces in mobile applications are visually attractive. And it’s always a huge pleasure for developers to work together with talented designers. However an excellent UI requires even more - the art of telling a story.\n My presentation is about enhancing the UI and UX by taking advantage of animation capabilities in React Native. During the talk, you will learn in practice when and how animations should be used and how to create an alluring story for the application.' 252 | }, 253 | { 254 | day: 6, 255 | time: '12:30', 256 | date: '2018-09-06 12:30', 257 | speaker: 'Vladimir Novick', 258 | company: '', 259 | title: 'Demystifying complex animations creation process in React Native', 260 | agenda: 261 | 'While Animations can be created in React Native pretty easily, real world mobile apps require a combination of several layers of animations, gestures and micro interaction animations to make user experience stand out. In this talk we will walk through the process from defining complex animation, to implementing it by going through all stages of animation creation process.' 262 | }, 263 | { 264 | day: 6, 265 | time: '13:00', 266 | date: '2018-09-06 13:00', 267 | speaker: 'Lunch', 268 | company: '', 269 | title: '', 270 | agenda: '' 271 | }, 272 | { 273 | day: 6, 274 | time: '14:00', 275 | date: '2018-09-06 14:00', 276 | speaker: 'David Vacca', 277 | company: 'FACEBOOK', 278 | title: 'The state of React Native', 279 | agenda: 280 | 'This presentation will focus on core projects the React Native team is working on, particularly regarding the re-architecture of the framework.' 281 | }, 282 | { 283 | day: 6, 284 | time: '14:30', 285 | date: '2018-09-06 14:30', 286 | speaker: 'Narendra Shetty', 287 | company: 'BOOKING.COM', 288 | title: 'A/B testing with React Native', 289 | agenda: 290 | '\n A/B testing is a common practice in web world. Should that button say "click here" or "learn more"? Will the layout of your homepage materially impact app downloads? Instead of going with your gut, let data drive your product development.\n I will be talking about how to do that in the app world using React Native, both for iOS and Android and the complexities around it. Also I plan to cover these aspects —
\n – Why should you use A/B testing in your business?
\n – What all can you test using A/B testing?
\n – How can you do efficient A/B testing of change in your product?
\n ' 291 | }, 292 | { 293 | day: 6, 294 | time: '15:00', 295 | date: '2018-09-06 15:00', 296 | speaker: 'Akshat Paul', 297 | company: 'MCKINSEY & COMPANY', 298 | title: 'Building apps for everyone - Accessibility with React Native', 299 | agenda: 300 | 'Developers constantly strive to make stunning apps which not only solve day to day problems but augment life of their users in a way that the app becomes an important part of their lives. The examples of such disruptive apps is endless but at the same time it’s also important that an app for many does not leave few behind. In our quest for building next great app Apple and Android both provides apis to keep accessibility integral part of application design. In this talk I’ll be introducing this concept to every React Native developer and give handson how they can make use of voiceover, accessibility properties and other accessibility elements part of their RN app to make an application inclusive for all. I am equally excited to talk and give demo on this topic since such reminders have far reaching effect on developer community when they build their next app.' 301 | }, 302 | { 303 | day: 6, 304 | time: '15:30', 305 | date: '2018-09-06 15:30', 306 | speaker: 'Coffee break', 307 | company: '', 308 | title: '', 309 | agenda: '' 310 | }, 311 | { 312 | day: 6, 313 | time: '16:00', 314 | date: '2018-09-06 16:00', 315 | speaker: 'Wouter van den Broek', 316 | company: 'SYNAPPZ MOBILE HEALTH', 317 | title: 'Getting to the Native in React Native', 318 | agenda: 319 | 'The Native part in React Native can look very scary and intimidated but is does not have to be that way, so in this talk you will see what the most frequent challenges are when working on the native side of React Native and how to conquer them.\n If you attend this session you see and learn how to solve a lot of native issues that pop up when working with React Native native code and modules.
\n - The basic of the React Native native module
\n What Is a native module in react native, what are all the moving parts, how do the function in the total process of react native and what is the potential
\n - Extending native modules
\n Native modules are easy extendable, you can use other languages like Swift or Kotlin and even other platforms (Windows/Mac). Also you can extend existing native modules form others if you want to
\n - Building
\n Showing what happens when building, how to solve build errors, solving dependencies and to release when building succeeded
\n - Tips & Tricks
\n Some more tips and tricks when working with native code in your react native project which I encountered in my career.\n ' 320 | }, 321 | { 322 | day: 6, 323 | time: '16:30', 324 | date: '2018-09-06 16:30', 325 | speaker: 'Q&A panel', 326 | company: '', 327 | title: '', 328 | agenda: 'Quinlan Jung, Expo' 329 | }, 330 | { 331 | day: 6, 332 | time: '17:15', 333 | date: '2018-09-06 17:15', 334 | speaker: 'Closing', 335 | company: '', 336 | title: '', 337 | agenda: '' 338 | } 339 | ]; 340 | export default data; 341 | -------------------------------------------------------------------------------- /app/src/data/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | format, 3 | parse, 4 | getTime, 5 | startOfWeek, 6 | addDays, 7 | isSameDay, 8 | setDate 9 | } from 'date-fns'; 10 | 11 | import slugify from 'slugify'; 12 | import { calendar } from '../theme'; 13 | import events from './events'; 14 | import speakers from './speakers'; 15 | 16 | const { CELL_NUM } = calendar; 17 | const DATE_FORMAT = 'YYYY-MM-dd HH:mm'; 18 | const baseDate = new Date(); 19 | const parseDate = str => parse(str, DATE_FORMAT, baseDate); 20 | 21 | const speakerLookup = speakers.reduce( 22 | (lookup, speaker) => ({ 23 | ...lookup, 24 | [slugify(speaker.name)]: speaker 25 | }), 26 | {} 27 | ); 28 | 29 | const normalisedEvents = events 30 | .map(event => { 31 | const day = event.day || null; 32 | const date = setDate(parseDate(event.date), day + 1); 33 | 34 | const title = event.title || null; 35 | const speaker = event.speaker || null; 36 | const company = event.company || null; 37 | 38 | // remove excess whitespace, preseve paragraps 39 | // (real smart code here 👇) 40 | const agenda = event.agenda 41 | ? event.agenda 42 | .trim() 43 | .replace('\n', '

') 44 | .replace(/\s+/g, ' ') 45 | .replace(/
/g, '\n') 46 | .replace(/

/g, '\n\n') 47 | : null; 48 | 49 | const duration = event.duration || 0.5; // in hours 50 | const profile = speaker ? speakerLookup[slugify(speaker)] : undefined; 51 | const isTalk = !!profile; 52 | 53 | if (!date || (!title && !speaker)) { 54 | return null; 55 | } 56 | return { 57 | slug: slugify(`${title} ${getTime(date)}`), 58 | title: title || speaker, 59 | isTalk, 60 | speaker: isTalk ? speaker : null, 61 | profile, 62 | company, 63 | agenda, 64 | date, 65 | duration 66 | }; 67 | }) 68 | .filter(Boolean); 69 | 70 | const monday = startOfWeek(normalisedEvents[0].date, { 71 | weekStartsOn: 1 72 | }); 73 | 74 | const data = Array.from({ length: CELL_NUM }).map((_, i) => { 75 | const date = addDays(monday, i); 76 | const weekday = format(date, 'EEEE').toUpperCase(); 77 | const label = weekday.slice(0, 3); 78 | const title = `${weekday.slice(3)}, ${date.getDate() - 1} ${format( 79 | date, 80 | 'MMM YYYY' 81 | )}`; 82 | 83 | const items = normalisedEvents.filter(event => isSameDay(date, event.date)); 84 | 85 | return { 86 | date, 87 | label, 88 | title, 89 | items 90 | }; 91 | }); 92 | 93 | export default data; 94 | -------------------------------------------------------------------------------- /app/src/data/speakers.js: -------------------------------------------------------------------------------- 1 | // scraped from https://react-native.eu/ 2 | 3 | /* 4 | copy( 5 | JSON.stringify( 6 | $$('.js-lightbox-speakers-content .speaker__content').map(item => { 7 | const photo = item.querySelector('img.speaker__photo'); 8 | const name = item.querySelector('.speaker__name'); 9 | const company = item.querySelector('.speaker__company'); 10 | const bio = item.querySelector('.speaker__bio'); 11 | 12 | return { 13 | photo: photo ? { 14 | uri: photo.src, 15 | width: photo.width, 16 | height: photo.height 17 | }: null, 18 | 19 | name: name.innerText, 20 | bio: bio ? bio.innerText.trim() : '', 21 | company: company.innerText 22 | }; 23 | }), 24 | null, 25 | 2 26 | ) 27 | ); 28 | */ 29 | 30 | export default [ 31 | { 32 | photo: { 33 | uri: 'https://react-native.eu/assets/img/data/speaker-sara.jpg', 34 | width: 1326, 35 | height: 1326 36 | }, 37 | name: 'Sara Vieira', 38 | bio: 39 | 'Developer Advocate at @YLDio. GraphQL and Open Source enthusiast. Conference Speaker and Airport expert. I am also into drums and horror movies.', 40 | company: 'YLD' 41 | }, 42 | { 43 | photo: { 44 | uri: 'https://react-native.eu/assets/img/data/speaker_david_vacca.png', 45 | width: 984, 46 | height: 984 47 | }, 48 | name: 'David Vacca', 49 | bio: 50 | 'David Vacca is a software engineer passionate about mobile development. He works in the React Native team at Facebook, where he is currently developing the next version of React Native.', 51 | company: 'Facebook' 52 | }, 53 | { 54 | photo: { 55 | uri: 'https://react-native.eu/assets/img/data/speaker-gant.png', 56 | width: 300, 57 | height: 300 58 | }, 59 | name: 'Gant Laborde', 60 | bio: 61 | 'Gant Laborde is a software consultant, adjunct professor, published author, and award-winning speaker. He volunteers as a\n mentor, an “open sourcerer”, and aspires to one day become a mad scientist. His React Native prowess is\n prevalent through blogs, videos, and maintainer status in popular repositories. His title is Chief Technology\n Strategist for Infinite Red. Follow Gant’s adventures at http://gantlaborde.com/', 62 | company: 'Infinite Red' 63 | }, 64 | { 65 | photo: { 66 | uri: 'https://react-native.eu/assets/img/data/speaker-quinlan.jpg', 67 | width: 982, 68 | height: 982 69 | }, 70 | name: 'Quinlan Jung', 71 | bio: 72 | 'Quinlan is a software engineer at Expo, an open source toolchain built around React Native. She’s previously worked on the\n Elastic Block Store at Amazon Web Services.', 73 | company: 'EXPO' 74 | }, 75 | { 76 | photo: { 77 | uri: 'https://react-native.eu/assets/img/data/speaker-harry.png', 78 | width: 412, 79 | height: 443 80 | }, 81 | name: 'Harry Tormey', 82 | bio: 83 | 'Harry is an experienced Mobile Software Engineer who has worked at prominent Bay Area companies such as Apple and Facebook.\n At Facebook he worked as an iOS engineer on both the search and commerce teams building customer facing\n features that shipped in the main Facebook app. At Apple he worked on the Core Audio team where he specialized\n in frameworks for multimedia Desktop and Mobile Applications. Harry has also shipped a number of React\n Native applications and runs a consultancy that specializes in mobile development and training. Harry has\n trained and consulted at organizations ranging in size from startups to fortune 500 companies.', 84 | company: '' 85 | }, 86 | { 87 | photo: { 88 | uri: 'https://react-native.eu/assets/img/data/speaker-samuli.png', 89 | width: 2816, 90 | height: 2816 91 | }, 92 | name: 'Samuli Hakoniemi', 93 | bio: 94 | 'Samuli is a long time entrepreneur who\'s in deep love with React Native. He works mostly with start-ups and also keeps himself\n busy by organising events in Finland.', 95 | company: 'Punos Mobile' 96 | }, 97 | { 98 | photo: { 99 | uri: 'https://react-native.eu/assets/img/data/speaker-jani.png', 100 | width: 500, 101 | height: 500 102 | }, 103 | name: 'Jani Eväkallio', 104 | bio: 105 | 'Jani, a veteran of the Frontend Framework wars, has earned his battle scars writing JavaScript on the bleeding edge for the\n last decade. Today, he leads a team building cross-stack React mobile and web apps at Formidable London.\n He gets childishly excited about beautiful user interactions and design tools. In his spare time, he performs\n improvised comedy and speaks at conferences advocating for the React ecosystem, particularly React Native\n and GraphQL.', 106 | company: 'Formidable Labs' 107 | }, 108 | { 109 | photo: { 110 | uri: 'https://react-native.eu/assets/img/data/speaker-mgrabowski.png', 111 | width: 1573, 112 | height: 1559 113 | }, 114 | name: 'Mike Grabowski', 115 | bio: '', 116 | company: 'Callstack' 117 | }, 118 | { 119 | photo: { 120 | uri: 'https://react-native.eu/assets/img/data/speaker-eric.png', 121 | width: 896, 122 | height: 896 123 | }, 124 | name: 'Eric Vicenti', 125 | bio: 126 | 'Creator of Aven, a full-stack framework for web and React Native apps. Author and maintainer of React Navigation with Expo.\n Formerly on Facebook’s open source team and the React Native team.', 127 | company: '' 128 | }, 129 | { 130 | photo: { 131 | uri: 'https://react-native.eu/assets/img/data/speaker-kmagiera.png', 132 | width: 818, 133 | height: 818 134 | }, 135 | name: 'Krzysztof Magiera', 136 | bio: 137 | 'Ex-facebooker and co-creator of React Native. Now as a head of mobile at Software Mansion working on performance focused\n improvements for animations and gesture handling to React Native and Expo.', 138 | company: 'Software Mansion' 139 | }, 140 | { 141 | photo: { 142 | uri: 'https://react-native.eu/assets/img/data/speaker-mhargett.png', 143 | width: 1920, 144 | height: 1920 145 | }, 146 | name: 'Matt Hargett', 147 | bio: 148 | 'Matt Hargett has been programming for over 30 years. He lives in San Francisco with his husband, daughter, and dog. His music can be found on all digital storefronts as “the making of the making of”.', 149 | company: 'PlayStation' 150 | }, 151 | { 152 | photo: { 153 | uri: 'https://react-native.eu/assets/img/data/speaker-schmiela.jpeg', 154 | width: 460, 155 | height: 460 156 | }, 157 | name: 'Stanisław Chmiela', 158 | bio: 159 | 'Currently a Software Mansioner happy to tackle and solve all the problems I face! Jumped into Expo almost a year ago and have been happy to help accelerate mobile app development since then. Previously worked on React frontends, Ruby and Elixir backends and both RN and native iOS mobile apps.', 160 | company: 'Software Mansion' 161 | }, 162 | { 163 | photo: { 164 | uri: 'https://react-native.eu/assets/img/data/speaker-pakshat.jpg', 165 | width: 400, 166 | height: 400 167 | }, 168 | name: 'Akshat Paul', 169 | bio: 170 | 'Akshat Paul is a developer & Author of the book React Native iOS development essentials with Apress and Rubymotion book with Packtpub (and Technical Reviewer for many titles over years). He has extensive experience of mobile & web development and has delivered many enterprise and consumer applications over the years. In other avatars, Akshat frequently speaks at conferences and meetups on various technologies. He has given talks at The DevTheory India, devops@Scale Amsterdam, RubyConfIndia, #inspect-RubyMotion Conference brussels and was KeyNote speaker at technology leadership events in Bangkok & KL on TDD. Besides writing code Akshat spends time with his family, is an avid reader and obsessive about healthy eating.', 171 | company: 'McKinsey & Company' 172 | }, 173 | { 174 | photo: { 175 | uri: 'https://react-native.eu/assets/img/data/speaker-vladimir.jpg', 176 | width: 400, 177 | height: 400 178 | }, 179 | name: 'Vladimir Novick', 180 | bio: 181 | 'Software Architect, independent consultant, worldwide speaker, co-organizer of ReactJS Israel, Author of "React Native - Build mobile apps with JavaScript" book and several workshops and courses. He brings years of experience in JavaScript ecosystem and is one of early React and React Native adopters. On daily basis Vladimir works in Web, Mobile, VR/AR and IoT fields both for customers and on personal projects. Previously he worked in Sports, Gaming & Video industries as Lead Architect & Team Leader', 182 | company: '' 183 | }, 184 | { 185 | photo: { 186 | uri: 'https://react-native.eu/assets/img/data/speaker-nshetty.jpeg', 187 | width: 250, 188 | height: 250 189 | }, 190 | name: 'Narendra Shetty', 191 | bio: 192 | 'Narendra is a Frontend developer at Booking.com. React / React Native enthusiast. He cares about performance and believes that performance is a key for conversion.', 193 | company: 'Booking.com' 194 | }, 195 | { 196 | photo: { 197 | uri: 'https://react-native.eu/assets/img/data/speaker-wbroek.png', 198 | width: 360, 199 | height: 358 200 | }, 201 | name: 'Wouter van den Broek', 202 | bio: 203 | 'Mobile developer for more than 15 years, moving from SMS, I-mode, Symbian to iOS, Android and now React Native. Coming from web development (both front and backend) which made me appreciate React Native even more. Building apps for over 10 years with always special needs as high interest, that’s why I landed in the world of eHealth and mHealth. Making apps for patients and caregivers to create that \'dent in the universe\' for them.', 204 | company: 'Synappz Mobile Health' 205 | }, 206 | { 207 | photo: { 208 | uri: 'https://react-native.eu/assets/img/data/speaker-rpietruszewski.jpg', 209 | width: 2320, 210 | height: 2320 211 | }, 212 | name: 'Radek Pietruszewski', 213 | bio: 214 | 'Software writer, proud generalist, senior skeptic. Leads iOS and Mac development at Nozbe. His background is in native development\n for Apple platforms, but for the past two years he\'s been working on next-generation apps built with\n React Native. You can follow Radek at @radexp and his blog on radex.io', 215 | company: 'Nozbe' 216 | }, 217 | { 218 | photo: { 219 | uri: 'https://react-native.eu/assets/img/data/speaker-rafael.jpg', 220 | width: 754, 221 | height: 731 222 | }, 223 | name: 'Rafael de Oleza', 224 | bio: 225 | 'Frontend developer with more than 10 years of experience and enthusiastic focus on bringing the developer experience of the Web to Mobile development. He’s part of the JavaScript Foundation team at Facebook where he maintains Metro, the React Native bundler.', 226 | company: 'Facebook' 227 | }, 228 | { 229 | photo: { 230 | uri: 'https://react-native.eu/assets/img/data/speaker-roy.jpg', 231 | width: 512, 232 | height: 512 233 | }, 234 | name: 'Roy Derks', 235 | bio: 236 | 'Roy Derks has been working as a Full-Stack Developer for multiple startups over the last 5 years. Over a year ago he co-founded SwitchBay, where he holds the position of Technical Founder and CTO. Avidly tweets about everything related to JavaScript using #javascriptEverywhere.', 237 | company: 'Hackteam' 238 | }, 239 | { 240 | photo: { 241 | uri: 'https://react-native.eu/assets/img/data/speaker-pavlo.jpg', 242 | width: 1365, 243 | height: 1365 244 | }, 245 | name: 'Pavlo Babenko', 246 | bio: 247 | 'Pavlo is a software consultant and a web developer. His path started from the Rails development six years ago, then went to web frontend (React.js, Ember.js) and for past two years, he’s pioneering React Native in his company.', 248 | company: 'Axsy' 249 | } 250 | ]; 251 | -------------------------------------------------------------------------------- /app/src/theme.js: -------------------------------------------------------------------------------- 1 | import { Platform, Dimensions, StyleSheet } from 'react-native'; 2 | 3 | const viewport = Dimensions.get('window'); 4 | 5 | const CELL_NUM = 7; 6 | const SIDEBAR_WIDTH = viewport.width / 8; 7 | const CONTAINER_WIDTH = viewport.width - SIDEBAR_WIDTH; 8 | const CELL_WIDTH = CONTAINER_WIDTH / CELL_NUM; 9 | const HOUR_HEIGHT = CELL_WIDTH * 2; 10 | const PINCH_MAGNITUDE = 0.5; 11 | const SIDEBAR_OFFSET = -8; 12 | 13 | export const isAndroid = Platform.OS === 'android'; 14 | export const isIOS = Platform.OS === 'ios'; 15 | 16 | export const calendar = { 17 | CELL_NUM, 18 | SIDEBAR_WIDTH, 19 | CONTAINER_WIDTH, 20 | CELL_WIDTH, 21 | HOUR_HEIGHT, 22 | SIDEBAR_OFFSET, 23 | PINCH_MAGNITUDE 24 | }; 25 | 26 | export const colors = { 27 | header: '#fbf9fb', 28 | text: '#191819', 29 | label: '#7c7284', 30 | stroke: '#d3cbd8', 31 | strokeRgb: [211, 203, 216], 32 | card: '#e53d40', 33 | inactive: '#a7abb7' 34 | }; 35 | 36 | export const sizes = { 37 | hairline: StyleSheet.hairlineWidth, 38 | small: 6, 39 | mid: 12, 40 | large: 24, 41 | cellWidth: `${CELL_WIDTH}px` 42 | }; 43 | -------------------------------------------------------------------------------- /app/src/views/Details/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import { Transition } from 'react-navigation-fluid-transitions'; 3 | 4 | import { BackButton, Container } from '../../components/misc'; 5 | import { Scrollable, Photo, HeaderContent, Header, Title, Speaker, Agenda } from './styles'; 6 | 7 | const Details = ({ navigation }) => { 8 | const event = navigation.getParam('event', {}); 9 | const photo = event.profile && event.profile.photo; 10 | 11 | return ( 12 | 13 | 14 | {photo && ( 15 | 20 | )} 21 | 22 |

23 | 24 | {event.title} 25 | {event.speaker ? {event.speaker} : null} 26 | 27 |
28 | 29 | {event.agenda || 'Nothing much to say about this.'} 30 | 31 | 32 | navigation.goBack()} /> 33 | 34 | ); 35 | }; 36 | 37 | export default Details; 38 | -------------------------------------------------------------------------------- /app/src/views/Details/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import { Constants } from 'expo'; 3 | 4 | import * as theme from '../../theme'; 5 | 6 | const { statusBarHeight } = Constants; 7 | 8 | export const Scrollable = styled.ScrollView``; 9 | 10 | export const Header = styled.View` 11 | background-color: ${p => 12 | p.isTalk ? theme.colors.card : theme.colors.inactive}; 13 | padding: ${p => (p.isTalk ? 20 : statusBarHeight + 80)}px 10px 20px 10px; 14 | `; 15 | 16 | export const HeaderContent = styled.View` 17 | background-color: transparent; 18 | `; 19 | 20 | export const Title = styled.Text` 21 | color: white; 22 | font-size: 16px; 23 | font-weight: bold; 24 | margin-bottom: 4px; 25 | `; 26 | 27 | export const Photo = styled.Image` 28 | width: 100%; 29 | aspect-ratio: ${p => p.width / p.height}; 30 | `; 31 | 32 | export const Speaker = styled.Text` 33 | color: white; 34 | font-size: 12px; 35 | font-style: italic; 36 | `; 37 | 38 | export const Agenda = styled.Text` 39 | font-size: 14px; 40 | line-height: 18px; 41 | padding: 20px 10px 40px 10px; 42 | `; 43 | -------------------------------------------------------------------------------- /app/src/views/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Animated from 'react-native-reanimated'; 3 | 4 | import data from '../data'; 5 | import * as theme from '../theme'; 6 | 7 | import { Container, Header } from '../components/misc'; 8 | import EventItems from '../components/EventItems'; 9 | import CalendarLayout from '../components/CalendarLayout'; 10 | import CalendarColumns from '../components/CalendarColumns'; 11 | 12 | const initialScrollY = 8 * theme.calendar.HOUR_HEIGHT; 13 | 14 | class Home extends Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.indexState = new Animated.Value(-1); 19 | this.zoomState = new Animated.Value(0); 20 | } 21 | 22 | render() { 23 | const { 24 | navigation: { navigate } 25 | } = this.props; 26 | 27 | return ( 28 | 29 |
x.label)} 33 | titles={data.map(x => x.title)} 34 | /> 35 | 36 | 40 | {({ gestureHandlerRef }) => ( 41 | 46 | {i => } 47 | 48 | )} 49 | 50 | 51 | ); 52 | } 53 | } 54 | 55 | export default Home; 56 | -------------------------------------------------------------------------------- /app/src/views/Navigator.js: -------------------------------------------------------------------------------- 1 | import { createStackNavigator as createNavigator } from 'react-navigation'; 2 | // import { FluidNavigator as createNavigator } from 'react-navigation-fluid-transitions'; 3 | 4 | import Home from './Home'; 5 | import Details from './Details'; 6 | 7 | export default createNavigator( 8 | { 9 | Home: { 10 | screen: Home 11 | }, 12 | Event: { 13 | screen: Details 14 | } 15 | }, 16 | { 17 | headerMode: 'none' 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /app/vendor/react-native-reanimated-v1.0.0-alpha.3.patchexpo29.2.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-native-interactions-workshop/aaed09c882669fd37496b8795ecc3fe66be9f036/app/vendor/react-native-reanimated-v1.0.0-alpha.3.patchexpo29.2.tgz -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [Settings] 2 | ID = "1c344ce1-aca2-41e5-98c4-0a2b94201bae" 3 | 4 | [Build] 5 | Publish = "" 6 | Functions = "" 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-interactions-workshop", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "mdx-deck workshop.mdx", 8 | "build": "mdx-deck build --no-html workshop.mdx -d .build", 9 | "deploy": "netlifyctl deploy -P ./.build -n rneu-interactions" 10 | }, 11 | "devDependencies": { 12 | "mdx-deck": "^1.7.1", 13 | "mdx-deck-code-surfer": "^0.4.0", 14 | "raw-loader": "^0.5.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /presentation/components/Provider.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react" 2 | import { injectGlobal } from "styled-components" 3 | import DefaultProvider from "mdx-deck/dist/Provider" 4 | 5 | injectGlobal` 6 | code.scroll-content .token-line .token.comment:first-of-type { 7 | opacity: 0.3; 8 | } 9 | ` 10 | 11 | const Provider = props => ( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | export default Provider; 20 | -------------------------------------------------------------------------------- /presentation/components/Video.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withDeck } from "mdx-deck"; 3 | import styled, { css } from "styled-components"; 4 | 5 | const Wrapper = styled.div` 6 | position: relative; 7 | max-height: 75vh; 8 | display: flex; 9 | flex-direction: row; 10 | align-items: stretch; 11 | z-index: -1; 12 | 13 | & ~ * { 14 | display: inline-block; 15 | background: ${p => p.theme.colors.background}; 16 | border-radius: 5px; 17 | margin-left: -6px; 18 | padding: 4px 6px; 19 | } 20 | `; 21 | 22 | const Video = styled.video.attrs({ 23 | playsInline: true, 24 | muted: true, 25 | loop: true, 26 | autoPlay: true, 27 | preload: "metadata" 28 | })` 29 | transition: transform 1.7s ease-in-out; 30 | 31 | ${p => p.isActive && css` 32 | transform: scale(1.6) translateY(5vh); 33 | `} 34 | `; 35 | 36 | const VideoMain = ({ deck: { active }, url }) => ( 37 | 38 | 42 | 43 | ); 44 | 45 | export default withDeck(VideoMain); 46 | -------------------------------------------------------------------------------- /presentation/snippets/animated.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from React; 2 | import { View, Button, Animated } from 'react-native'; 3 | 4 | class Hideable extends Component { 5 | // define an animated value 6 | animation = new Animated.Value(1); 7 | 8 | state = { 9 | hidden: false 10 | }; 11 | 12 | hide = () => { 13 | // run animation imperatively 14 | Animated.spring(this.animation, { toValue: 0 }).start(() => { 15 | this.setState({ hidden: true }) 16 | }); 17 | } 18 | 19 | render() { 20 | // interpolate 21 | const opacity = this.animation.interpolate({ 22 | inputRange: [0, 0.5, 1], 23 | outputRange: [0, 1, 1] 24 | }); 25 | 26 | return ( 27 | 28 | 29 |