├── first ├── .watchmanconfig ├── assets │ ├── icon.png │ ├── beach.jpg │ ├── forest.jpg │ ├── splash.png │ └── mountain.jpg ├── babel.config.js ├── src │ ├── screens │ │ ├── ComponentsScreen.js │ │ ├── ImageScreen.js │ │ ├── ColourScreen.js │ │ ├── ListsScreen.js │ │ ├── CounterScreen.js │ │ ├── HomeScreen.js │ │ ├── InputScreen.js │ │ ├── SquareScreen.js │ │ └── BoxScreen.js │ └── components │ │ ├── ImageDetail.js │ │ └── ColourAdjuster.js ├── app.json ├── .eslintrc.js ├── package.json └── App.js ├── rn-starter ├── .watchmanconfig ├── assets │ ├── icon.png │ └── splash.png ├── babel.config.js ├── .gitignore ├── src │ └── screens │ │ └── HomeScreen.js ├── App.js ├── app.json └── package.json ├── blog ├── assets │ ├── icon.png │ └── splash.png ├── src │ ├── api │ │ └── posts.js │ ├── context │ │ ├── createDataContext.js │ │ └── index.js │ ├── screens │ │ ├── CreateScreen.js │ │ ├── EditScreen.js │ │ ├── ShowScreen.js │ │ └── IndexScreen.js │ └── components │ │ └── PostForm.js ├── babel.config.js ├── .expo-shared │ └── assets.json ├── app.json ├── .eslintrc.js ├── App.js └── package.json ├── food ├── assets │ ├── icon.png │ ├── splash.png │ ├── placeholder.png │ └── placeholder.xcf ├── babel.config.js ├── .expo-shared │ └── assets.json ├── src │ ├── api │ │ └── yelp.js │ ├── hooks │ │ └── useYelp.js │ ├── components │ │ ├── Restaurant.js │ │ ├── SearchBar.js │ │ └── RestaurantList.js │ └── screens │ │ ├── RestaurantScreen.js │ │ └── SearchScreen.js ├── App.js ├── app.json ├── .eslintrc.js └── package.json ├── tracks ├── assets │ ├── icon.png │ └── splash.png ├── babel.config.js ├── .expo-shared │ └── assets.json ├── src │ ├── navigationRef.js │ ├── screens │ │ ├── LoadingScreen.js │ │ ├── AccountScreen.js │ │ ├── TrackListScreen.js │ │ ├── TrackDetailScreen.js │ │ ├── LoginScreen.js │ │ ├── SignupScreen.js │ │ └── TrackCreateScreen.js │ ├── components │ │ ├── Spacer.js │ │ ├── NavLink.js │ │ ├── TrackForm.js │ │ ├── Map.js │ │ └── AuthForm.js │ ├── api │ │ └── tracker.js │ ├── hooks │ │ ├── useSaveTrack.js │ │ └── useLocation.js │ ├── context │ │ ├── createDataContext.js │ │ ├── Tracks.js │ │ ├── Geo.js │ │ └── Auth.js │ └── _mockLocation.js ├── app.json ├── .eslintrc.js ├── package.json └── App.js ├── blog-server ├── db.json ├── package.json └── package-lock.json ├── track-server ├── .eslintrc.js ├── package.json └── src │ ├── models │ ├── Track.js │ └── User.js │ ├── middleware │ └── requireAuth.js │ ├── routes │ ├── tracks.js │ └── auth.js │ └── index.js ├── LICENSE ├── .gitignore └── README.md /first/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /rn-starter/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /blog/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/blog/assets/icon.png -------------------------------------------------------------------------------- /first/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/first/assets/icon.png -------------------------------------------------------------------------------- /food/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/food/assets/icon.png -------------------------------------------------------------------------------- /blog/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/blog/assets/splash.png -------------------------------------------------------------------------------- /first/assets/beach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/first/assets/beach.jpg -------------------------------------------------------------------------------- /first/assets/forest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/first/assets/forest.jpg -------------------------------------------------------------------------------- /first/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/first/assets/splash.png -------------------------------------------------------------------------------- /food/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/food/assets/splash.png -------------------------------------------------------------------------------- /tracks/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/tracks/assets/icon.png -------------------------------------------------------------------------------- /tracks/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/tracks/assets/splash.png -------------------------------------------------------------------------------- /first/assets/mountain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/first/assets/mountain.jpg -------------------------------------------------------------------------------- /rn-starter/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/rn-starter/assets/icon.png -------------------------------------------------------------------------------- /food/assets/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/food/assets/placeholder.png -------------------------------------------------------------------------------- /food/assets/placeholder.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/food/assets/placeholder.xcf -------------------------------------------------------------------------------- /rn-starter/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianNicholls/React-Native-Course/HEAD/rn-starter/assets/splash.png -------------------------------------------------------------------------------- /blog/src/api/posts.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export default axios.create({ 4 | baseURL: 'http://ff3e4ab6.ngrok.io', 5 | }); 6 | -------------------------------------------------------------------------------- /blog/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /first/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /food/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /tracks/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /rn-starter/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /rn-starter/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | *.orig.* 9 | web-build/ 10 | web-report/ 11 | -------------------------------------------------------------------------------- /blog/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true, 3 | "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true 4 | } -------------------------------------------------------------------------------- /food/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true, 3 | "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true 4 | } -------------------------------------------------------------------------------- /tracks/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true, 3 | "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true 4 | } -------------------------------------------------------------------------------- /food/src/api/yelp.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import config from '../../config'; 3 | 4 | export default axios.create({ 5 | baseURL: 'https://api.yelp.com/v3/businesses', 6 | headers: { 7 | Authorization: `Bearer ${config.API_KEY}`, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /tracks/src/navigationRef.js: -------------------------------------------------------------------------------- 1 | import { NavigationActions } from 'react-navigation'; 2 | 3 | let navigator; 4 | 5 | export const setNavigator = nav => { 6 | navigator = nav; 7 | }; 8 | 9 | export const navigate = (routeName, params) => { 10 | navigator.dispatch(NavigationActions.navigate({ routeName, params })); 11 | }; 12 | -------------------------------------------------------------------------------- /rn-starter/src/screens/HomeScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, StyleSheet } from 'react-native'; 3 | 4 | const HomeScreen = () => { 5 | return HomeScreen; 6 | }; 7 | 8 | const styles = StyleSheet.create({ 9 | text: { 10 | fontSize: 30 11 | } 12 | }); 13 | 14 | export default HomeScreen; 15 | -------------------------------------------------------------------------------- /tracks/src/screens/LoadingScreen.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | import { useAuth } from '../context/Auth'; 4 | 5 | const LoadingScreen = () => { 6 | const { authTryLocalLogin } = useAuth(); 7 | 8 | useEffect(() => { 9 | authTryLocalLogin(); 10 | }, []); 11 | 12 | return null; 13 | }; 14 | 15 | export default LoadingScreen; 16 | -------------------------------------------------------------------------------- /rn-starter/App.js: -------------------------------------------------------------------------------- 1 | import { createStackNavigator, createAppContainer } from 'react-navigation'; 2 | import HomeScreen from './src/screens/HomeScreen'; 3 | 4 | const navigator = createStackNavigator( 5 | { 6 | Home: HomeScreen 7 | }, 8 | { 9 | initialRouteName: 'Home', 10 | defaultNavigationOptions: { 11 | title: 'App' 12 | } 13 | } 14 | ); 15 | 16 | export default createAppContainer(navigator); 17 | -------------------------------------------------------------------------------- /blog-server/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "posts": [ 3 | { 4 | "id": "111", 5 | "title": "First past the post - from JSON", 6 | "content": "Yes, I am content" 7 | }, 8 | { 9 | "id": "222", 10 | "title": "Second in command - from JSON", 11 | "content": "Contentious, eh?" 12 | }, 13 | { 14 | "title": "New post updated", 15 | "content": "Content edited", 16 | "id": "eff83Nj" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /tracks/src/components/Spacer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { View, StyleSheet } from 'react-native'; 5 | 6 | const Spacer = ({ children }) => { 7 | return {children}; 8 | }; 9 | 10 | const styles = StyleSheet.create({ 11 | spacer: { 12 | margin: 15, 13 | }, 14 | }); 15 | 16 | Spacer.propTypes = { 17 | children: PropTypes.element, 18 | }; 19 | 20 | export default Spacer; 21 | -------------------------------------------------------------------------------- /track-server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es6: true, 6 | node: true, 7 | }, 8 | extends: 'eslint:recommended', 9 | globals: { 10 | Atomics: 'readonly', 11 | SharedArrayBuffer: 'readonly', 12 | }, 13 | parserOptions: { 14 | ecmaVersion: 2018, 15 | }, 16 | rules: { 17 | 'no-console': 'off', 18 | 'linebreak-style': ['error', 'unix'], 19 | quotes: ['error', 'single'], 20 | semi: ['error', 'always'], 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /tracks/src/api/tracker.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { AsyncStorage } from 'react-native'; 3 | 4 | const instance = axios.create({ 5 | baseURL: 'http://192.168.1.192:3001', 6 | }); 7 | 8 | instance.interceptors.request.use( 9 | async config => { 10 | const token = await AsyncStorage.getItem('token'); 11 | 12 | if (token) config.headers.Authorization = `Bearer ${token}`; 13 | 14 | return config; 15 | }, 16 | err => { 17 | return Promise.reject(err); 18 | } 19 | ); 20 | 21 | export default instance; 22 | -------------------------------------------------------------------------------- /tracks/src/hooks/useSaveTrack.js: -------------------------------------------------------------------------------- 1 | import { useGeo } from '../context/Geo'; 2 | import { useTracks } from '../context/Tracks'; 3 | import { navigate } from '../navigationRef'; 4 | 5 | export default () => { 6 | const { 7 | state: { trackName, locations }, 8 | geoReset, 9 | } = useGeo(); 10 | const { tracksCreate } = useTracks(); 11 | 12 | const saveTrack = async () => { 13 | await tracksCreate(trackName, locations); 14 | geoReset(); 15 | navigate('TrackList'); 16 | }; 17 | 18 | return [saveTrack]; 19 | }; 20 | -------------------------------------------------------------------------------- /blog-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "db": "json-server -w db.json", 8 | "tunnel": "ngrok http 3000", 9 | "server": "concurrently \"npm run db\" \"npm run tunnel\"" 10 | }, 11 | "keywords": [], 12 | "author": "Julian Nicholls (https://reallybigshoe.co.uk/)", 13 | "license": "ISC", 14 | "dependencies": { 15 | "json-server": "^1.0.0-beta.2", 16 | "ngrok": "^5.0.0-beta.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /food/App.js: -------------------------------------------------------------------------------- 1 | import { createAppContainer } from 'react-navigation'; 2 | import { createStackNavigator } from 'react-navigation-stack'; 3 | 4 | import SearchScreen from './src/screens/SearchScreen'; 5 | import RestaurantScreen from './src/screens/RestaurantScreen'; 6 | 7 | const navigator = createStackNavigator( 8 | { 9 | Search: SearchScreen, 10 | RestaurantShow: RestaurantScreen, 11 | }, 12 | { 13 | initialRouteName: 'Search', 14 | defaultNavigationOptions: { 15 | title: 'Business Search', 16 | }, 17 | } 18 | ); 19 | 20 | export default createAppContainer(navigator); 21 | -------------------------------------------------------------------------------- /first/src/screens/ComponentsScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, StyleSheet, View } from 'react-native'; 3 | 4 | const ComponentsScreen = () => { 5 | const name = 'Julian'; 6 | 7 | return ( 8 | 9 | Components 10 | My name is {name} 11 | 12 | ); 13 | }; 14 | 15 | const styles = StyleSheet.create({ 16 | titleStyle: { 17 | fontSize: 45, 18 | textAlign: 'center', 19 | marginBottom: 10, 20 | }, 21 | textStyle: { 22 | fontSize: 24, 23 | }, 24 | }); 25 | 26 | export default ComponentsScreen; 27 | -------------------------------------------------------------------------------- /track-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "track-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon src/index.js" 9 | }, 10 | "keywords": [], 11 | "author": "Julian Nicholls (https://reallybigshoe.co.uk/)", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt": "^5.0.0", 15 | "express": "^4.17.1", 16 | "jsonwebtoken": "^9.0.0", 17 | "mongoose": "^8.8.3" 18 | }, 19 | "devDependencies": { 20 | "eslint": "^8.43.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /track-server/src/models/Track.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const PointSchema = new mongoose.Schema({ 4 | timestamp: Number, 5 | coords: { 6 | latitude: Number, 7 | longitude: Number, 8 | altitude: Number, 9 | accuracy: Number, 10 | heading: Number, 11 | speed: Number, 12 | }, 13 | }); 14 | 15 | const TrackSchema = new mongoose.Schema({ 16 | userId: { 17 | type: mongoose.Schema.Types.ObjectId, 18 | ref: 'User', 19 | }, 20 | name: { 21 | type: String, 22 | required: true, 23 | }, 24 | locations: [PointSchema], 25 | }); 26 | 27 | mongoose.model('Track', TrackSchema); 28 | -------------------------------------------------------------------------------- /first/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "exmulti", 4 | "slug": "exmulti", 5 | "privacy": "public", 6 | "sdkVersion": "33.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android", 10 | "web" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /food/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Food", 4 | "slug": "food", 5 | "privacy": "public", 6 | "sdkVersion": "36.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android", 10 | "web" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /blog/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Simple Blog", 4 | "slug": "blog", 5 | "privacy": "public", 6 | "sdkVersion": "36.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android", 10 | "web" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rn-starter/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "exmulti", 4 | "slug": "exmulti", 5 | "privacy": "public", 6 | "sdkVersion": "33.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android", 10 | "web" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /tracks/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Tracks", 4 | "slug": "tracks", 5 | "privacy": "public", 6 | "sdkVersion": "36.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android", 10 | "web" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /track-server/src/middleware/requireAuth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const mongoose = require('mongoose'); 3 | 4 | const User = mongoose.model('User'); 5 | 6 | module.exports = (req, res, next) => { 7 | const { authorization } = req.headers; 8 | 9 | if (!authorization) return res.status(401).send('You must be logged in'); 10 | 11 | const token = authorization.replace('Bearer ', ''); // Just the token 12 | 13 | jwt.verify(token, 'MYSUPERLONGSECRETKEYISTHISSTRING', async (err, payload) => { 14 | if (err) return res.status(401).send('You must be logged in'); 15 | 16 | const { userId } = payload; 17 | 18 | const user = await User.findById(userId); 19 | 20 | req.user = user; 21 | 22 | next(); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /tracks/src/context/createDataContext.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default (reducer, actions, defaultState) => { 5 | const Context = React.createContext(); 6 | 7 | const Provider = ({ children }) => { 8 | const [state, dispatch] = useReducer(reducer, defaultState); 9 | 10 | const boundActions = {}; 11 | 12 | for (const key in actions) { 13 | boundActions[key] = actions[key](dispatch); 14 | } 15 | 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | }; 22 | 23 | Provider.propTypes = { 24 | children: PropTypes.element.isRequired, 25 | }; 26 | 27 | return { Context, Provider }; 28 | }; 29 | -------------------------------------------------------------------------------- /blog/src/context/createDataContext.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default (reducer, actions, initialValue) => { 5 | const Context = React.createContext(); 6 | 7 | const Provider = ({ children }) => { 8 | const [state, dispatch] = useReducer(reducer, initialValue); 9 | 10 | // action fixup 11 | const boundActions = {}; 12 | 13 | for (let key in actions) { 14 | boundActions[key] = actions[key](dispatch); 15 | } 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | Provider.propTypes = { 25 | children: PropTypes.element, 26 | }; 27 | 28 | return { Context, Provider }; 29 | }; 30 | -------------------------------------------------------------------------------- /food/src/hooks/useYelp.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | import yelp from '../api/yelp'; 4 | 5 | export default () => { 6 | const [results, setResults] = useState([]); 7 | const [error, setError] = useState(''); 8 | 9 | const search = async term => { 10 | try { 11 | const response = await yelp.get('/search', { 12 | params: { 13 | term, 14 | limit: 50, 15 | location: 'Bournemouth', 16 | radius: 10000, // 10km 17 | }, 18 | }); 19 | 20 | setResults(response.data.businesses); 21 | } catch (err) { 22 | setError('Could not load restaurants. Please try again later.'); 23 | } 24 | }; 25 | 26 | useEffect(() => { 27 | search('indian'); 28 | }, []); 29 | 30 | return [search, results, error]; 31 | }; 32 | -------------------------------------------------------------------------------- /tracks/src/components/NavLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { TouchableOpacity, StyleSheet } from 'react-native'; 4 | import { Text } from 'react-native-elements'; 5 | 6 | import { withNavigation } from 'react-navigation'; 7 | 8 | const NavLink = ({ navigation, text, to }) => { 9 | return ( 10 | navigation.navigate(to)}> 11 | {text} 12 | 13 | ); 14 | }; 15 | 16 | const styles = StyleSheet.create({ 17 | link: { 18 | color: 'blue', 19 | }, 20 | }); 21 | 22 | NavLink.propTypes = { 23 | navigation: PropTypes.object.isRequired, 24 | text: PropTypes.string.isRequired, 25 | to: PropTypes.string.isRequired, 26 | }; 27 | 28 | export default withNavigation(NavLink); 29 | -------------------------------------------------------------------------------- /rn-starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "expo": "^52.0.25", 12 | "react": "16.8.3", 13 | "react-dom": "^16.8.6", 14 | "react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz", 15 | "react-native-web": "^0.11.4", 16 | "react-navigation": "^5.0.0" 17 | }, 18 | "devDependencies": { 19 | "babel-preset-expo": "^9.5.0", 20 | "eslint": "^6.7.2", 21 | "eslint-plugin-jsx-a11y": "^6.2.3", 22 | "eslint-plugin-react": "^7.17.0", 23 | "eslint-plugin-react-hooks": "^2.3.0" 24 | 25 | }, 26 | "private": true 27 | } 28 | -------------------------------------------------------------------------------- /blog/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: ['eslint:recommended', 'plugin:react/recommended'], 8 | globals: { 9 | Atomics: 'readonly', 10 | SharedArrayBuffer: 'readonly', 11 | }, 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | }, 19 | plugins: ['react'], 20 | rules: { 21 | 'no-console': 'off', 22 | 'linebreak-style': ['error', 'unix'], 23 | quotes: ['error', 'single'], 24 | semi: ['error', 'always'], 25 | 'jsx-a11y/href-no-hash': 'off', 26 | // 'jsx-a11y/anchor-is-valid': ['warn', { aspects: ['invalidHref'] }], 27 | // 'react-hooks/rules-of-hooks': 'error', 28 | // 'react-hooks/exhaustive-deps': 'warn', 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /first/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: ['eslint:recommended', 'plugin:react/recommended'], 8 | globals: { 9 | Atomics: 'readonly', 10 | SharedArrayBuffer: 'readonly', 11 | }, 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | }, 19 | plugins: ['react'], 20 | rules: { 21 | 'no-console': 'off', 22 | 'linebreak-style': ['error', 'unix'], 23 | quotes: ['error', 'single'], 24 | semi: ['error', 'always'], 25 | 'jsx-a11y/href-no-hash': 'off', 26 | // 'jsx-a11y/anchor-is-valid': ['warn', { aspects: ['invalidHref'] }], 27 | // 'react-hooks/rules-of-hooks': 'error', 28 | // 'react-hooks/exhaustive-deps': 'warn', 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /food/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: ['eslint:recommended', 'plugin:react/recommended'], 8 | globals: { 9 | Atomics: 'readonly', 10 | SharedArrayBuffer: 'readonly', 11 | }, 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | }, 19 | plugins: ['react'], 20 | rules: { 21 | 'no-console': 'off', 22 | 'linebreak-style': ['error', 'unix'], 23 | quotes: ['error', 'single'], 24 | semi: ['error', 'always'], 25 | 'jsx-a11y/href-no-hash': 'off', 26 | // 'jsx-a11y/anchor-is-valid': ['warn', { aspects: ['invalidHref'] }], 27 | // 'react-hooks/rules-of-hooks': 'error', 28 | // 'react-hooks/exhaustive-deps': 'warn', 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /tracks/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: ['eslint:recommended', 'plugin:react/recommended'], 8 | globals: { 9 | Atomics: 'readonly', 10 | SharedArrayBuffer: 'readonly', 11 | }, 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | }, 19 | plugins: ['react'], 20 | rules: { 21 | 'no-console': 'off', 22 | 'linebreak-style': ['error', 'unix'], 23 | quotes: ['error', 'single'], 24 | semi: ['error', 'always'], 25 | 'jsx-a11y/href-no-hash': 'off', 26 | // 'jsx-a11y/anchor-is-valid': ['warn', { aspects: ['invalidHref'] }], 27 | // 'react-hooks/rules-of-hooks': 'error', 28 | // 'react-hooks/exhaustive-deps': 'warn', 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /first/src/screens/ImageScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, StyleSheet } from 'react-native'; 3 | 4 | import ImageDetail from '../components/ImageDetail'; 5 | 6 | const ImageScreen = () => { 7 | return ( 8 | 9 | Images 10 | 15 | 20 | 25 | 26 | ); 27 | }; 28 | 29 | const styles = StyleSheet.create({ 30 | titleStyle: { 31 | fontSize: 30, 32 | textAlign: 'center', 33 | }, 34 | }); 35 | 36 | export default ImageScreen; 37 | -------------------------------------------------------------------------------- /tracks/src/screens/AccountScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | import { Text, Button } from 'react-native-elements'; 4 | import { SafeAreaView } from 'react-navigation'; 5 | import { FontAwesome } from '@expo/vector-icons'; 6 | 7 | import { useAuth } from '../context/Auth'; 8 | import Spacer from '../components/Spacer'; 9 | 10 | const AccountScreen = () => { 11 | const { authLogout } = useAuth(); 12 | 13 | return ( 14 | 15 | Your account 16 | 17 | 29 | 33 | 34 | ); 35 | }; 36 | 37 | const styles = StyleSheet.create({ 38 | title: { 39 | fontSize: 30, 40 | textAlign: 'center', 41 | marginBottom: 40, 42 | }, 43 | counter: { 44 | backgroundColor: '#dddddd', 45 | borderWidth: 1, 46 | borderColor: '#333333', 47 | fontSize: 100, 48 | padding: 20, 49 | textAlign: 'center', 50 | fontWeight: 'bold', 51 | color: '#ff4328', 52 | marginBottom: 40, 53 | }, 54 | }); 55 | 56 | export default CounterScreen; 57 | -------------------------------------------------------------------------------- /tracks/src/hooks/useLocation.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { 3 | requestPermissionsAsync, 4 | watchPositionAsync, 5 | Accuracy, 6 | } from 'expo-location'; 7 | 8 | export default (shouldTrack, callback) => { 9 | const [error, setError] = useState(null); 10 | 11 | useEffect(() => { 12 | let subscriber = null; 13 | 14 | const startWatching = async () => { 15 | // console.log('startWatching called'); 16 | try { 17 | // Currently requestPermissionsAsync does not throw an error on iOS, 18 | // so we check the response here. 19 | const response = await requestPermissionsAsync(); 20 | 21 | if (!response.granted) return setError('denied'); 22 | 23 | subscriber = await watchPositionAsync( 24 | { 25 | accuracy: Accuracy.BestForNavigation, 26 | timeInterval: 2000, 27 | distanceInterval: 20, 28 | }, 29 | callback 30 | ); 31 | } catch (err) { 32 | console.log(err); 33 | setError(err); 34 | } 35 | }; 36 | 37 | if (shouldTrack) startWatching(); 38 | else { 39 | // console.log('else', subscriber); 40 | if (subscriber) subscriber.remove(); 41 | subscriber = null; 42 | } 43 | 44 | return () => { 45 | // console.log('cleanup', subscriber); 46 | if (subscriber) subscriber.remove(); 47 | subscriber = null; 48 | }; 49 | }, [shouldTrack]); 50 | 51 | return [error]; 52 | }; 53 | -------------------------------------------------------------------------------- /food/src/components/RestaurantList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native'; 4 | import { withNavigation } from 'react-navigation'; 5 | 6 | import Restaurant from './Restaurant'; 7 | 8 | const RestaurantList = ({ title, restaurants, navigation }) => { 9 | // Bail out if there's no restaurants to show 10 | if (restaurants.length === 0) return null; 11 | 12 | const renderItem = ({ item }) => ( 13 | navigation.navigate('RestaurantShow', { id: item.id })} 15 | > 16 | 17 | 18 | ); 19 | 20 | return ( 21 | 22 | {title} 23 | item.id} 28 | renderItem={renderItem} 29 | /> 30 | 31 | ); 32 | }; 33 | 34 | const styles = StyleSheet.create({ 35 | container: { 36 | marginBottom: 10, 37 | }, 38 | title: { 39 | marginLeft: 10, 40 | marginBottom: 5, 41 | fontSize: 18, 42 | fontWeight: 'bold', 43 | }, 44 | }); 45 | 46 | RestaurantList.propTypes = { 47 | title: PropTypes.string.isRequired, 48 | restaurants: PropTypes.array.isRequired, 49 | navigation: PropTypes.object.isRequired, 50 | }; 51 | 52 | export default withNavigation(RestaurantList); 53 | -------------------------------------------------------------------------------- /track-server/src/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const mongoose = require('mongoose'); 3 | const jwt = require('jsonwebtoken'); 4 | 5 | const router = express.Router(); 6 | const User = mongoose.model('User'); 7 | 8 | router.post('/signup', async (req, res) => { 9 | const { email, password } = req.body; 10 | 11 | if (!email || password.length < 6) { 12 | return res.status(422).send('Email address and password must be valid'); 13 | } 14 | 15 | try { 16 | const user = new User({ email, password }); 17 | 18 | await user.save(); 19 | 20 | const token = jwt.sign( 21 | { userId: user._id }, 22 | 'MYSUPERLONGSECRETKEYISTHISSTRING' 23 | ); 24 | res.send({ token }); 25 | } catch (err) { 26 | return res.status(422).send(`Could not create user: ${err}`); 27 | } 28 | }); 29 | 30 | router.post('/login', async (req, res) => { 31 | const { email, password } = req.body; 32 | 33 | if (!email || !password) 34 | return res.status(401).send('Invalid email address or password'); 35 | 36 | const user = await User.findOne({ email }); 37 | 38 | if (!user) return res.status(401).send('Invalid email address or password'); 39 | 40 | try { 41 | await user.comparePassword(password); 42 | 43 | const token = jwt.sign( 44 | { userId: user._id }, 45 | 'MYSUPERLONGSECRETKEYISTHISSTRING' 46 | ); 47 | res.send({ token }); 48 | } catch (err) { 49 | return res.status(401).send('Invalid email address or password'); 50 | } 51 | }); 52 | 53 | module.exports = router; 54 | -------------------------------------------------------------------------------- /tracks/src/screens/TrackCreateScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { SafeAreaView, withNavigationFocus } from 'react-navigation'; 4 | import { StyleSheet } from 'react-native'; 5 | import { Text } from 'react-native-elements'; 6 | import { FontAwesome } from '@expo/vector-icons'; 7 | 8 | import Map from '../components/Map'; 9 | import { useGeo } from '../context/Geo'; 10 | import useLocation from '../hooks/useLocation'; 11 | import Spacer from '../components/Spacer'; 12 | import TrackForm from '../components/TrackForm'; 13 | 14 | import '../_mockLocation'; // Fake locations 15 | 16 | const TrackCreateScreen = ({ isFocused }) => { 17 | const { state: geo, geoAddLocation } = useGeo(); 18 | const [error] = useLocation(isFocused || geo.recording, geoAddLocation); 19 | 20 | return ( 21 | 22 | Create a new track 23 | 24 | {error !== null && ( 25 | Please enable location services 26 | )} 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | const styles = StyleSheet.create({ 35 | error: { 36 | fontSize: 16, 37 | color: 'red', 38 | }, 39 | }); 40 | 41 | TrackCreateScreen.navigationOptions = { 42 | title: 'Add Track', 43 | tabBarIcon: , 44 | }; 45 | 46 | TrackCreateScreen.propTypes = { 47 | isFocused: PropTypes.bool.isRequired, 48 | }; 49 | 50 | export default withNavigationFocus(TrackCreateScreen); 51 | -------------------------------------------------------------------------------- /food/src/screens/RestaurantScreen.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { FlatList, Text, Image, StyleSheet } from 'react-native'; 4 | 5 | import yelp from '../api/yelp'; 6 | 7 | const RestaurantScreen = ({ navigation }) => { 8 | const id = navigation.getParam('id'); 9 | const [restaurant, setRestaurant] = useState(null); 10 | 11 | const getRestaurant = async id => { 12 | const response = await yelp.get(`/${id}`); 13 | 14 | setRestaurant(response.data); 15 | }; 16 | 17 | useEffect(() => { 18 | getRestaurant(id); 19 | }, []); 20 | 21 | const renderPhoto = ({ item }) => ( 22 | 23 | ); 24 | 25 | if (!restaurant) return Loading...; 26 | 27 | return ( 28 | <> 29 | {restaurant.name} 30 | {restaurant.photos.length > 0 ? ( 31 | item} 34 | renderItem={renderPhoto} 35 | /> 36 | ) : ( 37 | (No Photos) 38 | )} 39 | 40 | ); 41 | }; 42 | 43 | const styles = StyleSheet.create({ 44 | name: { 45 | fontSize: 20, 46 | textAlign: 'center', 47 | marginTop: 10, 48 | }, 49 | image: { 50 | height: 300, 51 | width: 300, 52 | marginLeft: 'auto', 53 | marginRight: 'auto', 54 | marginTop: 10, 55 | }, 56 | }); 57 | 58 | RestaurantScreen.propTypes = { 59 | navigation: PropTypes.object.isRequired, 60 | }; 61 | 62 | export default RestaurantScreen; 63 | -------------------------------------------------------------------------------- /first/src/screens/HomeScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; 4 | import { withNavigation } from 'react-navigation'; 5 | 6 | const NavButton = withNavigation(({ text, destination, navigation }) => ( 7 | navigation.navigate(destination)} 10 | > 11 | {text} 12 | 13 | )); 14 | 15 | const HomeScreen = () => { 16 | return ( 17 | 18 | Home 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | const styles = StyleSheet.create({ 32 | buttonContainer: { 33 | alignItems: 'center', 34 | }, 35 | title: { 36 | fontSize: 30, 37 | marginBottom: 30, 38 | }, 39 | button: { 40 | alignItems: 'center', 41 | backgroundColor: '#abd7ed', 42 | borderColor: '#247ca8', 43 | borderWidth: 1, 44 | borderRadius: 4, 45 | padding: 8, 46 | width: '80%', 47 | marginBottom: 10, 48 | }, 49 | buttonText: { 50 | color: 'blue', 51 | fontSize: 20, 52 | }, 53 | }); 54 | 55 | NavButton.propTypes = { 56 | text: PropTypes.string.isRequired, 57 | destination: PropTypes.string.isRequired, 58 | navigation: PropTypes.object, 59 | }; 60 | 61 | export default HomeScreen; 62 | -------------------------------------------------------------------------------- /first/src/screens/InputScreen.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { View, Text, TextInput, StyleSheet } from 'react-native'; 3 | 4 | const InputScreen = () => { 5 | const [name, setName] = useState(''); 6 | const [password, setPassword] = useState(''); 7 | 8 | return ( 9 | 10 | Input 11 | 12 | Name 13 | setName(text)} 19 | /> 20 | 21 | Password 22 | setPassword(text)} 29 | /> 30 | 31 | Name: {name} 32 | 33 | Password:{' '} 34 | {password.length < 6 ? ( 35 | Too short 36 | ) : ( 37 | OK 38 | )} 39 | 40 | 41 | ); 42 | }; 43 | 44 | const styles = StyleSheet.create({ 45 | title: { 46 | fontSize: 30, 47 | textAlign: 'center', 48 | marginBottom: 15, 49 | }, 50 | input: { 51 | fontSize: 20, 52 | marginLeft: 15, 53 | marginRight: 15, 54 | borderColor: '#000080', 55 | borderWidth: 1, 56 | padding: 3, 57 | }, 58 | label: { 59 | fontSize: 20, 60 | marginTop: 15, 61 | marginLeft: 15, 62 | }, 63 | name: { 64 | fontSize: 20, 65 | margin: 15, 66 | }, 67 | bad: { color: 'red' }, 68 | good: { color: 'green' }, 69 | }); 70 | 71 | export default InputScreen; 72 | -------------------------------------------------------------------------------- /tracks/src/components/AuthForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { StyleSheet } from 'react-native'; 4 | import { Text, Input, Button } from 'react-native-elements'; 5 | 6 | import Spacer from './Spacer'; 7 | 8 | const AuthForm = ({ headerText, errorMessage, buttonText, buttonAction }) => { 9 | const [email, setEmail] = useState(''); 10 | const [password, setPassword] = useState(''); 11 | 12 | return ( 13 | <> 14 | 15 | {headerText} 16 | 17 | 26 | 27 | 37 | 38 | {errorMessage !== '' && {errorMessage}} 39 | 40 | 41 |