├── 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 |
18 |
19 |
20 | );
21 | };
22 |
23 | AccountScreen.navigationOptions = {
24 | title: 'Account',
25 | tabBarIcon: ,
26 | };
27 |
28 | const styles = StyleSheet.create({
29 | title: {
30 | fontSize: 32,
31 | },
32 | });
33 |
34 | export default AccountScreen;
35 |
--------------------------------------------------------------------------------
/first/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 | "@react-native-community/masked-view": "0.1.10",
12 | "expo": "~52.0.14",
13 | "expo-status-bar": "~1.0.4",
14 | "react": "16.13.1",
15 | "react-dom": "16.13.1",
16 | "react-native": "https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz",
17 | "react-native-gesture-handler": "~1.10.2",
18 | "react-native-reanimated": "~2.10.0",
19 | "react-native-safe-area-context": "3.2.0",
20 | "react-native-screens": "~3.0.0",
21 | "react-native-web": "~0.13.12",
22 | "react-navigation": "^4.4.4",
23 | "react-navigation-stack": "^2.10.4"
24 | },
25 | "devDependencies": {
26 | "@babel/core": "^7.9.0"
27 | },
28 | "private": true
29 | }
30 |
--------------------------------------------------------------------------------
/blog/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { createAppContainer } from 'react-navigation';
4 | import { createStackNavigator } from 'react-navigation-stack';
5 |
6 | import { BlogProvider } from './src/context';
7 |
8 | import IndexScreen from './src/screens/IndexScreen';
9 | import ShowScreen from './src/screens/ShowScreen';
10 | import CreateScreen from './src/screens/CreateScreen';
11 | import EditScreen from './src/screens/EditScreen';
12 |
13 | const navigator = createStackNavigator(
14 | {
15 | Index: IndexScreen,
16 | Show: ShowScreen,
17 | Create: CreateScreen,
18 | Edit: EditScreen,
19 | },
20 | {
21 | initialRouteName: 'Index',
22 | defaultNavigationOptions: {
23 | title: 'Simple Blog',
24 | },
25 | }
26 | );
27 |
28 | const BlogApp = createAppContainer(navigator);
29 |
30 | const App = () => {
31 | return (
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default App;
39 |
--------------------------------------------------------------------------------
/blog/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 | "axios": "^1.12.0",
12 | "expo": "~52.0.7",
13 | "prop-types": "^15.7.2",
14 | "react": "~16.9.0",
15 | "react-dom": "~16.9.0",
16 | "react-native": "https://github.com/expo/react-native/archive/sdk-36.0.0.tar.gz",
17 | "react-native-gesture-handler": "~1.5.0",
18 | "react-native-reanimated": "~2.10.0",
19 | "react-native-web": "~0.11.7",
20 | "react-navigation": "^4.0.10",
21 | "react-navigation-stack": "^1.10.3"
22 | },
23 | "devDependencies": {
24 | "babel-preset-expo": "~9.5.0",
25 | "eslint": "^6.7.2",
26 | "eslint-plugin-jsx-a11y": "^6.2.3",
27 | "eslint-plugin-react": "^7.17.0",
28 | "eslint-plugin-react-hooks": "^2.3.0"
29 | },
30 | "private": true
31 | }
32 |
--------------------------------------------------------------------------------
/blog/src/screens/CreateScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { View, Text, StyleSheet } from 'react-native';
5 |
6 | import { useBlog } from '../context';
7 | import PostForm from '../components/PostForm';
8 |
9 | const CreateScreen = ({ navigation }) => {
10 | const { addPost } = useBlog();
11 |
12 | return (
13 |
14 | New Post
15 |
16 | {
19 | addPost(title, content, () => navigation.goBack());
20 | }}
21 | />
22 |
23 | );
24 | };
25 |
26 | const styles = StyleSheet.create({
27 | view: {
28 | padding: 10,
29 | },
30 | title: {
31 | fontSize: 20,
32 | textAlign: 'center',
33 | marginBottom: 10,
34 | },
35 | });
36 |
37 | CreateScreen.propTypes = {
38 | navigation: PropTypes.object.isRequired,
39 | };
40 |
41 | export default CreateScreen;
42 |
--------------------------------------------------------------------------------
/food/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 | "axios": "^1.12.0",
12 | "expo": "~52.0.7",
13 | "prop-types": "^15.7.2",
14 | "react": "~16.9.0",
15 | "react-dom": "~16.9.0",
16 | "react-native": "https://github.com/expo/react-native/archive/sdk-36.0.0.tar.gz",
17 | "react-native-gesture-handler": "~1.5.0",
18 | "react-native-reanimated": "~2.10.0",
19 | "react-native-web": "~0.11.7",
20 | "react-navigation": "^4.0.10",
21 | "react-navigation-stack": "^1.10.3"
22 | },
23 | "devDependencies": {
24 | "babel-preset-expo": "~9.5.0",
25 | "eslint": "^6.7.2",
26 | "eslint-plugin-jsx-a11y": "^6.2.3",
27 | "eslint-plugin-react": "^7.17.0",
28 | "eslint-plugin-react-hooks": "^2.3.0"
29 | },
30 | "private": true
31 | }
32 |
--------------------------------------------------------------------------------
/first/src/components/ImageDetail.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { View, Text, Image, StyleSheet } from 'react-native';
4 |
5 | const ImageDetail = ({ title, source, score }) => {
6 | return (
7 |
8 |
9 |
10 | {title}
11 | {score && Score: {score}/10}
12 |
13 |
14 | );
15 | };
16 |
17 | const styles = StyleSheet.create({
18 | view: {
19 | marginTop: 20,
20 | flexDirection: 'row',
21 | alignItems: 'center',
22 | justifyContent: 'space-around',
23 | },
24 | image: {
25 | borderColor: 'black',
26 | borderWidth: 1,
27 | },
28 | caption: { fontSize: 20 },
29 | });
30 |
31 | ImageDetail.propTypes = {
32 | title: PropTypes.string.isRequired,
33 | source: PropTypes.number.isRequired,
34 | score: PropTypes.number,
35 | };
36 |
37 | export default ImageDetail;
38 |
--------------------------------------------------------------------------------
/tracks/src/_mockLocation.js:
--------------------------------------------------------------------------------
1 | import * as Location from 'expo-location';
2 |
3 | const tenMetersWithDegrees = 0.0001; // Appromimately 10m
4 | let prevLocation = { latitude: 50.733, longitude: -1.853 };
5 |
6 | const getLocation = () => {
7 | const location = {
8 | timestamp: Math.floor(Date.now() / 1000),
9 | coords: {
10 | speed: 0,
11 | heading: 0,
12 | accuracy: 5,
13 | altitudeAccuracy: 5,
14 | altitude: 5,
15 | latitude: prevLocation.latitude,
16 | longitude: prevLocation.longitude,
17 | },
18 | };
19 |
20 | if (Math.random() > 0.4) location.coords.latitude += tenMetersWithDegrees;
21 | if (Math.random() > 0.4) location.coords.longitude += tenMetersWithDegrees;
22 |
23 | prevLocation = location.coords;
24 |
25 | // console.log({ prev: prevLocation, coords: location.coords });
26 |
27 | return location;
28 | };
29 |
30 | setInterval(() => {
31 | Location.EventEmitter.emit('Expo.locationChanged', {
32 | watchId: Location._getCurrentWatchId(),
33 | location: getLocation(),
34 | });
35 | }, 2000);
36 |
--------------------------------------------------------------------------------
/track-server/src/routes/tracks.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const mongoose = require('mongoose');
3 |
4 | const requireAuth = require('../middleware/requireAuth');
5 |
6 | const router = express.Router();
7 | const Track = mongoose.model('Track');
8 |
9 | router.use(requireAuth);
10 |
11 | router.get('/tracks', async (req, res) => {
12 | try {
13 | const tracks = await Track.find({ userId: req.user.id });
14 |
15 | res.send(tracks);
16 | } catch (err) {
17 | console.error(err);
18 | res.status(500).send('An error occurred');
19 | }
20 | });
21 |
22 | router.post('/tracks', async (req, res) => {
23 | const { name, locations } = req.body;
24 |
25 | if (!name || !locations)
26 | return res.status(422).send('You must provide a name and a list of locations');
27 |
28 | try {
29 | const track = new Track({ userId: req.user.id, name, locations });
30 |
31 | await track.save();
32 |
33 | res.send(track);
34 | } catch (err) {
35 | console.error(err);
36 | res.status(422).send('An error occurred: ' + err.message);
37 | }
38 | });
39 |
40 | module.exports = router;
41 |
--------------------------------------------------------------------------------
/first/App.js:
--------------------------------------------------------------------------------
1 | import { createStackNavigator, createAppContainer } from 'react-navigation';
2 |
3 | import HomeScreen from './src/screens/HomeScreen';
4 | import ComponentsScreen from './src/screens/ComponentsScreen';
5 | import ListsScreen from './src/screens/ListsScreen';
6 | import ImageScreen from './src/screens/ImageScreen';
7 | import CounterScreen from './src/screens/CounterScreen';
8 | import ColourScreen from './src/screens/ColourScreen';
9 | import SquareScreen from './src/screens/SquareScreen';
10 | import InputScreen from './src/screens/InputScreen';
11 | import BoxScreen from './src/screens/BoxScreen';
12 |
13 | const navigator = createStackNavigator(
14 | {
15 | Home: HomeScreen,
16 | Components: ComponentsScreen,
17 | Lists: ListsScreen,
18 | Image: ImageScreen,
19 | Counter: CounterScreen,
20 | Colour: ColourScreen,
21 | Square: SquareScreen,
22 | Input: InputScreen,
23 | Box: BoxScreen,
24 | },
25 | {
26 | initialRouteName: 'Home',
27 | defaultNavigationOptions: {
28 | title: 'First App',
29 | },
30 | }
31 | );
32 |
33 | export default createAppContainer(navigator);
34 |
--------------------------------------------------------------------------------
/first/src/components/ColourAdjuster.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { View, Button, StyleSheet } from 'react-native';
4 |
5 | const ColourAdjuster = ({ colour, adjust }) => {
6 | return (
7 |
8 |
19 | );
20 | };
21 |
22 | const styles = StyleSheet.create({
23 | view: {
24 | flexDirection: 'row',
25 | justifyContent: 'space-evenly',
26 | backgroundColor: '#c0c0c0',
27 | width: '90%',
28 | padding: 7,
29 | borderColor: 'blue',
30 | borderWidth: 1,
31 | marginBottom: 7,
32 | marginLeft: 'auto',
33 | marginRight: 'auto',
34 | },
35 | });
36 |
37 | ColourAdjuster.propTypes = {
38 | colour: PropTypes.string.isRequired,
39 | adjust: PropTypes.func.isRequired,
40 | };
41 |
42 | export default ColourAdjuster;
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Julian Nicholls
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/track-server/src/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const mongoose = require('mongoose');
3 |
4 | require('./models/User');
5 | require('./models/Track');
6 |
7 | const authRouter = require('./routes/auth');
8 | const trackRouter = require('./routes/tracks');
9 | const requireAuth = require('./middleware/requireAuth');
10 |
11 | const app = express();
12 |
13 | app.use(express.json());
14 | app.use(authRouter);
15 | app.use(trackRouter);
16 |
17 | const port = process.env.PORT || 3001;
18 |
19 | const mongoUri = 'mongodb://localhost/track';
20 |
21 | mongoose.connect(mongoUri, {
22 | useNewUrlParser: true,
23 | useUnifiedTopology: true,
24 | useCreateIndex: true,
25 | });
26 |
27 | mongoose.connection.on('connected', () => {
28 | console.log('Connected to local Mongo');
29 | });
30 |
31 | mongoose.connection.on('error', err => {
32 | console.error('Error connecting to local Mongo:', err);
33 | });
34 |
35 | app.get('/', requireAuth, (req, res) => {
36 | res.send(`Track-Server Root: ${req.user.email}\n`);
37 | });
38 |
39 | app.listen(port, () => {
40 | console.log(`Track-Server listening on port ${port}`);
41 | });
42 |
--------------------------------------------------------------------------------
/tracks/src/components/TrackForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Input, Button } from 'react-native-elements';
3 |
4 | import Spacer from './Spacer';
5 | import { useGeo } from '../context/Geo';
6 | import useSaveTrack from '../hooks/useSaveTrack';
7 |
8 | const TrackForm = () => {
9 | const {
10 | state: { trackName, recording, locations },
11 | geoStartRecording,
12 | geoStopRecording,
13 | geoSetTrackName,
14 | } = useGeo();
15 | const [saveTrack] = useSaveTrack();
16 |
17 | return (
18 | <>
19 |
20 |
25 |
26 | {recording ? (
27 |
28 | ) : (
29 |
30 | )}
31 |
32 | {!recording && locations.length > 0 && trackName !== '' && (
33 |
34 | )}
35 | >
36 | );
37 | };
38 | export default TrackForm;
39 |
--------------------------------------------------------------------------------
/tracks/src/components/Map.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, ActivityIndicator } from 'react-native';
3 | import MapView, { Polyline, Circle } from 'react-native-maps';
4 |
5 | import { useGeo } from '../context/Geo';
6 |
7 | const Map = () => {
8 | const {
9 | state: { currentLocation, locations },
10 | } = useGeo();
11 |
12 | if (!currentLocation) {
13 | return ;
14 | }
15 |
16 | const coords = locations.map(({ coords }) => coords);
17 |
18 | const centre = {
19 | ...currentLocation.coords,
20 | latitudeDelta: 0.01,
21 | longitudeDelta: 0.01,
22 | };
23 |
24 | return (
25 |
26 |
27 |
33 |
34 | );
35 | };
36 |
37 | const styles = StyleSheet.create({
38 | map: {
39 | height: 300,
40 | },
41 | });
42 |
43 | export default Map;
44 |
--------------------------------------------------------------------------------
/blog/src/screens/EditScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { View, Text, StyleSheet } from 'react-native';
5 |
6 | import { useBlog } from '../context';
7 | import PostForm from '../components/PostForm';
8 |
9 | const EditScreen = ({ navigation }) => {
10 | const post = navigation.getParam('post');
11 |
12 | const { updatePost } = useBlog();
13 |
14 | return (
15 |
16 | Edit Post
17 |
18 |
23 | updatePost(post.id, title, content, () => navigation.navigate('Index'))
24 | }
25 | />
26 |
27 | );
28 | };
29 |
30 | const styles = StyleSheet.create({
31 | view: {
32 | padding: 10,
33 | },
34 | title: {
35 | fontSize: 20,
36 | textAlign: 'center',
37 | marginBottom: 10,
38 | },
39 | });
40 |
41 | EditScreen.propTypes = {
42 | navigation: PropTypes.object.isRequired,
43 | };
44 |
45 | export default EditScreen;
46 |
--------------------------------------------------------------------------------
/tracks/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 | "axios": "^1.12.0",
12 | "expo": "~52.0.15",
13 | "react": "~16.9.0",
14 | "react-dom": "~16.9.0",
15 | "react-native": "https://github.com/expo/react-native/archive/sdk-36.0.0.tar.gz",
16 | "react-native-elements": "^1.2.7",
17 | "react-native-gesture-handler": "~1.5.0",
18 | "react-native-reanimated": "~2.10.0",
19 | "react-native-web": "~0.11.7",
20 | "react-navigation": "^4.0.10",
21 | "react-navigation-stack": "1.10.3",
22 | "react-navigation-tabs": "^2.7.0",
23 | "react-native-maps": "0.26.1",
24 | "expo-location": "~8.0.0"
25 | },
26 | "devDependencies": {
27 | "@babel/core": "^7.0.0",
28 | "babel-preset-expo": "~9.5.0",
29 | "eslint": "^6.8.0",
30 | "eslint-plugin-jsx-a11y": "^6.2.3",
31 | "eslint-plugin-react": "^7.17.0",
32 | "eslint-plugin-react-hooks": "^2.3.0"
33 | },
34 | "private": true
35 | }
36 |
--------------------------------------------------------------------------------
/tracks/src/screens/TrackListScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { FlatList, TouchableOpacity } from 'react-native';
4 | import { NavigationEvents } from 'react-navigation';
5 | import { ListItem } from 'react-native-elements';
6 |
7 | import { useTracks } from '../context/Tracks';
8 |
9 | const TrackListScreen = ({ navigation }) => {
10 | const {
11 | state: { tracks },
12 | tracksLoad,
13 | } = useTracks();
14 |
15 | return (
16 | <>
17 |
18 | item._id}
21 | renderItem={({ item }) => (
22 | navigation.navigate('TrackDetail', { _id: item._id })}
24 | >
25 |
26 |
27 | )}
28 | />
29 | >
30 | );
31 | };
32 |
33 | TrackListScreen.navigationOptions = {
34 | title: 'Tracks',
35 | };
36 |
37 | TrackListScreen.propTypes = {
38 | navigation: PropTypes.object.isRequired,
39 | };
40 |
41 | export default TrackListScreen;
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 | *.swp
63 | config/
64 | .expo
65 |
--------------------------------------------------------------------------------
/track-server/src/models/User.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const bcrypt = require('bcrypt');
3 |
4 | const UserSchema = new mongoose.Schema({
5 | email: {
6 | type: String,
7 | unique: true,
8 | required: true,
9 | },
10 | password: {
11 | type: String,
12 | required: true,
13 | },
14 | });
15 |
16 | // On save, encrypt the password
17 | UserSchema.pre('save', function(next) {
18 | const user = this;
19 |
20 | if (!user.isModified('password')) next();
21 |
22 | bcrypt.genSalt(10, (err, salt) => {
23 | if (err) return next(err);
24 |
25 | bcrypt.hash(user.password, salt, (err, hash) => {
26 | if (err) return next(err);
27 |
28 | user.password = hash;
29 | next();
30 | });
31 | });
32 | });
33 |
34 | // Compare the current user password with a candidate password
35 | UserSchema.methods.comparePassword = function(candidate) {
36 | const user = this;
37 |
38 | return new Promise((resolve, reject) => {
39 | bcrypt.compare(candidate, user.password, (err, isMatch) => {
40 | if (err) return reject(err);
41 |
42 | if (!isMatch) reject(false);
43 |
44 | resolve(true);
45 | });
46 | });
47 | };
48 |
49 | mongoose.model('User', UserSchema);
50 |
--------------------------------------------------------------------------------
/first/src/screens/ColourScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { View, Text, Button, FlatList, StyleSheet } from 'react-native';
3 |
4 | const randomRGB = () => {
5 | const red = Math.floor(Math.random() * 256);
6 | const green = Math.floor(Math.random() * 256);
7 | const blue = Math.floor(Math.random() * 256);
8 |
9 | return `rgb(${red}, ${green}, ${blue})`;
10 | };
11 |
12 | const ColourScreen = () => {
13 | const [colours, setColours] = useState([]);
14 |
15 | const addBlock = () => {
16 | setColours([...colours, randomRGB()]);
17 | };
18 |
19 | const drawBlock = ({ item }) => (
20 |
21 | );
22 |
23 | const keyExtractor = item => item;
24 |
25 | return (
26 |
27 | Colours
28 |
29 |
34 |
35 | );
36 | };
37 |
38 | const styles = StyleSheet.create({
39 | title: {
40 | fontSize: 30,
41 | textAlign: 'center',
42 | marginBottom: 40,
43 | },
44 | });
45 |
46 | export default ColourScreen;
47 |
--------------------------------------------------------------------------------
/first/src/screens/ListsScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Text, FlatList, StyleSheet } from 'react-native';
3 |
4 | const ListsScreen = () => {
5 | const friends = [
6 | { name: 'Friend 1', age: 23 },
7 | { name: 'Friend 2', age: 27 },
8 | { name: 'Friend 3', age: 34 },
9 | { name: 'Friend 4', age: 19 },
10 | { name: 'Friend 5', age: 47 },
11 | { name: 'Friend 6', age: 53 },
12 | { name: 'Friend 7', age: 45 },
13 | { name: 'Friend 8', age: 25 },
14 | { name: 'Friend 9', age: 56 },
15 | ];
16 |
17 | const renderItem = ({ item }) => (
18 |
19 | {item.name} - Age {item.age}
20 |
21 | );
22 |
23 | const keyExtractor = friend => friend.name.slice(-1);
24 |
25 | return (
26 |
27 | List Screen
28 |
33 |
34 | );
35 | };
36 |
37 | const styles = StyleSheet.create({
38 | titleStyle: {
39 | fontSize: 30,
40 | textAlign: 'center',
41 | marginBottom: 10,
42 | },
43 | itemStyle: {
44 | fontSize: 20,
45 | marginVertical: 5,
46 | },
47 | });
48 |
49 | export default ListsScreen;
50 |
--------------------------------------------------------------------------------
/food/src/components/Restaurant.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { View, Text, Image, StyleSheet } from 'react-native';
5 |
6 | const Restaurant = ({ restaurant }) => {
7 | return (
8 |
9 | {restaurant.image_url ? (
10 |
15 | ) : (
16 |
21 | )}
22 | {restaurant.name}
23 |
24 | {restaurant.rating} stars, {restaurant.review_count} reviews
25 |
26 |
27 | );
28 | };
29 |
30 | const styles = StyleSheet.create({
31 | view: {
32 | marginLeft: 10,
33 | },
34 | image: {
35 | width: 200,
36 | height: 120,
37 | borderRadius: 4,
38 | marginBottom: 5,
39 | },
40 | title: {
41 | // textAlign: 'center',
42 | fontSize: 16,
43 | fontWeight: 'bold',
44 | },
45 | });
46 |
47 | Restaurant.propTypes = {
48 | restaurant: PropTypes.object.isRequired,
49 | };
50 |
51 | export default Restaurant;
52 |
--------------------------------------------------------------------------------
/tracks/src/screens/TrackDetailScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { StyleSheet } from 'react-native';
4 | import { Text } from 'react-native-elements';
5 | import MapView, { Polyline } from 'react-native-maps';
6 |
7 | import { useTracks } from '../context/Tracks';
8 |
9 | const TrackDetailScreen = ({ navigation }) => {
10 | const {
11 | state: { tracks },
12 | } = useTracks();
13 | const trackId = navigation.getParam('_id');
14 | const track = tracks.find(({ _id }) => _id === trackId);
15 | const locations = track.locations.map(({ coords }) => coords);
16 |
17 | return (
18 | <>
19 | {track.name}
20 |
21 |
30 |
31 |
32 | >
33 | );
34 | };
35 |
36 | const styles = StyleSheet.create({
37 | map: {
38 | marginTop: 15,
39 | height: 300,
40 | },
41 | });
42 |
43 | TrackDetailScreen.propTypes = {
44 | navigation: PropTypes.object.isRequired,
45 | };
46 |
47 | export default TrackDetailScreen;
48 |
--------------------------------------------------------------------------------
/tracks/src/screens/LoginScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { View, StyleSheet } from 'react-native';
4 | import { NavigationEvents } from 'react-navigation';
5 |
6 | import { useAuth } from '../context/Auth';
7 | import AuthForm from '../components/AuthForm';
8 | import Spacer from '../components/Spacer';
9 | import NavLink from '../components/NavLink';
10 |
11 | const LoginScreen = () => {
12 | const { state: auth, authClearError, authLogin } = useAuth();
13 |
14 | return (
15 |
16 |
17 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | LoginScreen.navigationOptions = () => ({
31 | header: null,
32 | });
33 |
34 | const styles = StyleSheet.create({
35 | container: {
36 | flex: 1,
37 | justifyContent: 'center',
38 | marginBottom: 250,
39 | },
40 | });
41 |
42 | LoginScreen.propTypes = {
43 | navigation: PropTypes.object.isRequired,
44 | };
45 |
46 | export default LoginScreen;
47 |
--------------------------------------------------------------------------------
/tracks/src/screens/SignupScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { View, StyleSheet } from 'react-native';
4 | import { NavigationEvents } from 'react-navigation';
5 |
6 | import { useAuth } from '../context/Auth';
7 | import AuthForm from '../components/AuthForm';
8 | import Spacer from '../components/Spacer';
9 | import NavLink from '../components/NavLink';
10 |
11 | const SignupScreen = () => {
12 | const { state: auth, authClearError, authSignup } = useAuth();
13 |
14 | return (
15 |
16 |
17 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | SignupScreen.navigationOptions = () => ({
31 | header: null,
32 | });
33 |
34 | const styles = StyleSheet.create({
35 | container: {
36 | flex: 1,
37 | justifyContent: 'center',
38 | marginBottom: 250,
39 | },
40 | });
41 |
42 | SignupScreen.propTypes = {
43 | navigation: PropTypes.object.isRequired,
44 | };
45 |
46 | export default SignupScreen;
47 |
--------------------------------------------------------------------------------
/food/src/components/SearchBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { View, TextInput, StyleSheet } from 'react-native';
4 | import { Feather } from '@expo/vector-icons';
5 |
6 | const SearchBar = ({ term, setTerm, startSearch }) => {
7 | return (
8 |
9 |
10 |
19 |
20 | );
21 | };
22 |
23 | const styles = StyleSheet.create({
24 | bar: {
25 | flexDirection: 'row',
26 | backgroundColor: '#f0f0f0',
27 | paddingVertical: 5,
28 | borderColor: '#d0d0d0',
29 | borderRadius: 5,
30 | marginHorizontal: 10,
31 | marginVertical: 10,
32 | },
33 | icon: {
34 | fontSize: 35,
35 | alignSelf: 'center',
36 | marginHorizontal: 10,
37 | },
38 | input: {
39 | flex: 1, // Doesn't seem necessary for me
40 | fontSize: 20,
41 | },
42 | });
43 |
44 | SearchBar.propTypes = {
45 | term: PropTypes.string.isRequired,
46 | setTerm: PropTypes.func.isRequired,
47 | startSearch: PropTypes.func.isRequired,
48 | };
49 |
50 | export default SearchBar;
51 |
--------------------------------------------------------------------------------
/blog/src/screens/ShowScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
5 | import { EvilIcons } from '@expo/vector-icons';
6 |
7 | const ShowScreen = ({ navigation }) => {
8 | const post = navigation.getParam('post');
9 |
10 | return (
11 | <>
12 |
13 | {post.title}
14 | {post.content}
15 |
16 | >
17 | );
18 | };
19 |
20 | ShowScreen.navigationOptions = ({ navigation }) => ({
21 | headerRight: (
22 |
24 | navigation.navigate('Edit', { post: navigation.getParam('post') })
25 | }
26 | >
27 |
28 |
29 | ),
30 | });
31 |
32 | const styles = StyleSheet.create({
33 | post: {
34 | padding: 10,
35 | borderWidth: 1,
36 | margin: 10,
37 | },
38 | title: {
39 | fontSize: 24,
40 | fontWeight: 'bold',
41 | color: '#000070',
42 | marginBottom: 7,
43 | },
44 | content: {
45 | fontSize: 16,
46 | },
47 | icon: {
48 | fontSize: 30,
49 | },
50 | });
51 |
52 | ShowScreen.propTypes = {
53 | navigation: PropTypes.object.isRequired,
54 | };
55 |
56 | export default ShowScreen;
57 |
--------------------------------------------------------------------------------
/tracks/src/context/Tracks.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import createDataContext from './createDataContext';
3 |
4 | import trackApi from '../api/tracker';
5 |
6 | const TRACKS_LOAD = 'TRACKS_LOAD';
7 | const TRACKS_CREATE = 'TRACKS_CREATE';
8 |
9 | const tracksLoad = dispatch => async () => {
10 | const response = await trackApi.get('/tracks');
11 |
12 | dispatch({ type: TRACKS_LOAD, tracks: response.data });
13 | };
14 |
15 | const tracksCreate = dispatch => async (name, locations) => {
16 | const response = await trackApi.post('/tracks', { name, locations });
17 |
18 | dispatch({ type: TRACKS_CREATE, track: response.data });
19 | };
20 |
21 | const tracksReducer = (state, action) => {
22 | switch (action.type) {
23 | case TRACKS_LOAD:
24 | return { tracks: action.tracks };
25 |
26 | case TRACKS_CREATE:
27 | return { tracks: [state.tracks, action.track] };
28 |
29 | default:
30 | return state;
31 | }
32 | };
33 |
34 | const { Context: TracksContext, Provider: TracksProvider } = createDataContext(
35 | tracksReducer,
36 | { tracksLoad, tracksCreate },
37 | { tracks: [] }
38 | );
39 |
40 | const useTracks = () => {
41 | const context = useContext(TracksContext);
42 |
43 | if (context === undefined)
44 | throw new Error('useTracks must be used inside a TracksProvider');
45 |
46 | return context;
47 | };
48 |
49 | export { TracksProvider, useTracks };
50 |
--------------------------------------------------------------------------------
/blog/src/components/PostForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { Text, TextInput, Button, StyleSheet } from 'react-native';
5 |
6 | const PostForm = ({
7 | initialTitle = '',
8 | initialContent = '',
9 | buttonText,
10 | onSubmit,
11 | }) => {
12 | const [title, setTitle] = useState(initialTitle);
13 | const [content, setContent] = useState(initialContent);
14 |
15 | return (
16 | <>
17 | Title
18 | setTitle(text)}
22 | />
23 | Content
24 | setContent(text)}
28 | />
29 | onSubmit(title, content)} />
30 | >
31 | );
32 | };
33 |
34 | const styles = StyleSheet.create({
35 | label: {
36 | fontSize: 18,
37 | marginBottom: 5,
38 | },
39 | input: {
40 | fontSize: 16,
41 | borderWidth: 1,
42 | padding: 5,
43 | marginBottom: 10,
44 | },
45 | });
46 |
47 | PostForm.propTypes = {
48 | initialTitle: PropTypes.string,
49 | initialContent: PropTypes.string,
50 | buttonText: PropTypes.string.isRequired,
51 | onSubmit: PropTypes.func.isRequired,
52 | };
53 |
54 | export default PostForm;
55 |
--------------------------------------------------------------------------------
/first/src/screens/CounterScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useReducer } from 'react';
2 | import { View, Text, Button, StyleSheet } from 'react-native';
3 |
4 | // This is the most pointless use of reducers ever
5 | const reducer = (counter, action) => {
6 | switch (action.type) {
7 | case 'INCREMENT':
8 | return counter + 1;
9 |
10 | case 'DECREMENT':
11 | return counter - 1;
12 |
13 | default:
14 | return counter;
15 | }
16 | };
17 |
18 | const CounterScreen = () => {
19 | const [counter, dispatch] = useReducer(reducer, 0);
20 |
21 | return (
22 |
23 | Counter
24 | {counter}
25 | dispatch({ type: 'INCREMENT' })}
28 | >
29 | dispatch({ type: 'DECREMENT' })}
32 | >
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 | buttonAction(email, password)} />
42 |
43 | >
44 | );
45 | };
46 |
47 | const styles = StyleSheet.create({
48 | error: {
49 | marginTop: 15,
50 | textAlign: 'center',
51 | fontSize: 16,
52 | color: 'red',
53 | },
54 | });
55 |
56 | AuthForm.propTypes = {
57 | headerText: PropTypes.string.isRequired,
58 | errorMessage: PropTypes.string.isRequired,
59 | buttonText: PropTypes.string.isRequired,
60 | buttonAction: PropTypes.func.isRequired,
61 | };
62 |
63 | export default AuthForm;
64 |
--------------------------------------------------------------------------------
/tracks/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createAppContainer, createSwitchNavigator } from 'react-navigation';
3 | import { createStackNavigator } from 'react-navigation-stack';
4 | import { createBottomTabNavigator } from 'react-navigation-tabs';
5 |
6 | import { AuthProvider } from './src/context/Auth';
7 | import { GeoProvider } from './src/context/Geo';
8 | import { TracksProvider } from './src/context/Tracks';
9 | import { FontAwesome } from '@expo/vector-icons';
10 |
11 | import LoadingScreen from './src/screens/LoadingScreen';
12 | import SignupScreen from './src/screens/SignupScreen';
13 | import LoginScreen from './src/screens/LoginScreen';
14 | import TrackCreateScreen from './src/screens/TrackCreateScreen';
15 | import AccountScreen from './src/screens/AccountScreen';
16 | import TrackListScreen from './src/screens/TrackListScreen';
17 | import TrackDetailScreen from './src/screens/TrackDetailScreen';
18 |
19 | import { setNavigator } from './src/navigationRef';
20 |
21 | const trackListFlow = createStackNavigator({
22 | TrackList: TrackListScreen,
23 | TrackDetail: TrackDetailScreen,
24 | });
25 |
26 | trackListFlow.navigationOptions = {
27 | title: 'Tracks',
28 | tabBarIcon: ,
29 | };
30 |
31 | const switchNavigator = createSwitchNavigator({
32 | Loading: LoadingScreen,
33 | loginFlow: createStackNavigator({
34 | Signup: SignupScreen,
35 | Login: LoginScreen,
36 | }),
37 | mainFlow: createBottomTabNavigator({
38 | trackListFlow,
39 | TrackCreate: TrackCreateScreen,
40 | Account: AccountScreen,
41 | }),
42 | });
43 |
44 | const TrackApp = createAppContainer(switchNavigator);
45 |
46 | const App = () => (
47 |
48 |
49 |
50 | setNavigator(navigator)} />
51 |
52 |
53 |
54 | );
55 |
56 | export default App;
57 |
--------------------------------------------------------------------------------
/blog/src/screens/IndexScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native';
4 | import { Feather } from '@expo/vector-icons';
5 |
6 | import { useBlog } from '../context';
7 |
8 | const IndexScreen = ({ navigation }) => {
9 | const { state: posts, loadPosts, deletePost } = useBlog();
10 |
11 | useEffect(() => {
12 | loadPosts();
13 | }, []);
14 |
15 | const renderItem = ({ item }) => (
16 | navigation.navigate('Show', { post: item })}>
17 |
18 | {item.title}
19 | deletePost(item.id)}>
20 |
21 |
22 |
23 |
24 | );
25 |
26 | renderItem.propTypes = {
27 | item: PropTypes.object.isRequired,
28 | };
29 |
30 | return (
31 | <>
32 | item.id}
35 | renderItem={renderItem}
36 | />
37 | >
38 | );
39 | };
40 |
41 | IndexScreen.navigationOptions = ({ navigation }) => ({
42 | headerRight: (
43 | navigation.navigate('Create')}>
44 |
45 |
46 | ),
47 | });
48 |
49 | const styles = StyleSheet.create({
50 | post: {
51 | flexDirection: 'row',
52 | justifyContent: 'space-between',
53 | paddingHorizontal: 10,
54 | paddingVertical: 10,
55 | borderBottomWidth: 1,
56 | borderColor: 'gray',
57 | },
58 | title: {
59 | fontSize: 18,
60 | color: '#000070',
61 | },
62 | icon: {
63 | fontSize: 20,
64 | },
65 | });
66 |
67 | IndexScreen.propTypes = {
68 | navigation: PropTypes.object.isRequired,
69 | };
70 |
71 | export default IndexScreen;
72 |
--------------------------------------------------------------------------------
/food/src/screens/SearchScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { ScrollView, Text, StyleSheet } from 'react-native';
3 |
4 | import SearchBar from '../components/SearchBar';
5 | import useYelp from '../hooks/useYelp';
6 | import RestaurantList from '../components/RestaurantList';
7 |
8 | const SearchScreen = () => {
9 | const [term, setTerm] = useState('');
10 | const [search, results, error] = useYelp();
11 |
12 | const restaurantsByPrice = price => {
13 | const priceLen = price.length;
14 |
15 | // In the UK, the £ (GBP) character is used instead of the $ (USD),
16 | // hence I'm matching by length and so many have no price valuation,
17 | // that I have created an unrated list.
18 | if (priceLen === 0) return results.filter(r => !r.price);
19 | else return results.filter(r => r.price && r.price.length === priceLen);
20 | };
21 |
22 | return (
23 | <>
24 | search(term)} />
25 | {error ? {error} : null}
26 | {results.length > 0 ? (
27 |
28 |
29 |
33 |
37 |
44 |
45 | ) : (
46 | No results found
47 | )}
48 | >
49 | );
50 | };
51 |
52 | const styles = StyleSheet.create({
53 | error: {
54 | alignItems: 'center',
55 | fontSize: 18,
56 | color: 'red',
57 | },
58 | info: {
59 | fontSize: 18,
60 | textAlign: 'center',
61 | marginTop: 20,
62 | },
63 | });
64 |
65 | export default SearchScreen;
66 |
--------------------------------------------------------------------------------
/tracks/src/context/Geo.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import createDataContext from './createDataContext';
3 |
4 | const GEO_ADD_LOCATION = 'GEO_ADD_LOCATION';
5 | const GEO_START = 'GEO_START';
6 | const GEO_STOP = 'GEO_STOP';
7 | const GEO_TRACK_NAME = 'GEO_TRACK_NAME';
8 | const GEO_RESET = 'GEO_RESET';
9 |
10 | const geoStartRecording = dispatch => () => {
11 | dispatch({ type: GEO_START });
12 | };
13 |
14 | const geoStopRecording = dispatch => () => {
15 | dispatch({ type: GEO_STOP });
16 | };
17 |
18 | const geoAddLocation = dispatch => location => {
19 | dispatch({ type: GEO_ADD_LOCATION, location });
20 | };
21 |
22 | const geoSetTrackName = dispatch => name => {
23 | dispatch({ type: GEO_TRACK_NAME, name });
24 | };
25 |
26 | const geoReset = dispatch => () => {
27 | dispatch({ type: GEO_RESET });
28 | };
29 |
30 | const geoReducer = (geo, action) => {
31 | switch (action.type) {
32 | case GEO_ADD_LOCATION:
33 | if (geo.recording)
34 | return {
35 | ...geo,
36 | locations: [...geo.locations, action.location],
37 | currentLocation: action.location,
38 | };
39 |
40 | return { ...geo, currentLocation: action.location };
41 |
42 | case GEO_START:
43 | return { ...geo, recording: true };
44 |
45 | case GEO_STOP:
46 | return { ...geo, recording: false };
47 |
48 | case GEO_TRACK_NAME:
49 | return { ...geo, trackName: action.name };
50 |
51 | case GEO_RESET:
52 | return { ...geo, trackName: '', locations: [] };
53 |
54 | default:
55 | return geo;
56 | }
57 | };
58 |
59 | const { Context: GeoContext, Provider: GeoProvider } = createDataContext(
60 | geoReducer,
61 | {
62 | geoStartRecording,
63 | geoStopRecording,
64 | geoAddLocation,
65 | geoSetTrackName,
66 | geoReset,
67 | },
68 | { recording: false, trackName: '', locations: [], currentLocation: null }
69 | );
70 |
71 | const useGeo = () => {
72 | const context = useContext(GeoContext);
73 |
74 | if (context === undefined)
75 | throw new Error('useGeo must be used inside a GeoProvider');
76 |
77 | return context;
78 | };
79 |
80 | export { GeoProvider, useGeo };
81 |
--------------------------------------------------------------------------------
/first/src/screens/SquareScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useReducer } from 'react';
2 | import { View, Text, StyleSheet } from 'react-native';
3 |
4 | import ColourAdjuster from '../components/ColourAdjuster';
5 |
6 | const INCREMENT = 5;
7 |
8 | // I have taken the object destructuring to a ridiculous extent here.
9 | // I would not recommend this.
10 | const reducer = (state, { type, delta }) => {
11 | const { max, min } = Math;
12 | const { red, green, blue } = state;
13 |
14 | delta *= INCREMENT;
15 |
16 | switch (type) {
17 | case 'CHANGE_RED':
18 | return { ...state, red: max(0, min(255, red + delta)) };
19 |
20 | case 'CHANGE_GREEN':
21 | return { ...state, green: max(0, min(255, green + delta)) };
22 |
23 | case 'CHANGE_BLUE':
24 | return { ...state, blue: max(0, min(255, blue + delta)) };
25 |
26 | default:
27 | return state;
28 | }
29 | };
30 | const SquareScreen = () => {
31 | const [state, dispatch] = useReducer(reducer, { red: 0, green: 0, blue: 0 });
32 |
33 | return (
34 |
35 | RGB Block
36 | dispatch({ type: 'CHANGE_RED', delta })}
39 | />
40 | dispatch({ type: 'CHANGE_GREEN', delta })}
43 | />
44 | dispatch({ type: 'CHANGE_BLUE', delta })}
47 | />
48 |
49 |
55 |
56 |
57 | {state.red}, {state.green}, {state.blue}
58 |
59 |
60 | );
61 | };
62 |
63 | const styles = StyleSheet.create({
64 | title: {
65 | fontSize: 30,
66 | textAlign: 'center',
67 | marginBottom: 30,
68 | },
69 | block: {
70 | width: 250,
71 | height: 250,
72 | marginTop: 10,
73 | marginLeft: 'auto',
74 | marginRight: 'auto',
75 | },
76 | legend: {
77 | fontSize: 20,
78 | textAlign: 'center',
79 | marginTop: 10,
80 | },
81 | });
82 |
83 | export default SquareScreen;
84 |
--------------------------------------------------------------------------------
/first/src/screens/BoxScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Text, StyleSheet } from 'react-native';
3 |
4 | const BoxScreen = () => {
5 | return (
6 |
7 |
8 | Boxes
9 |
10 |
11 |
12 | Text one
13 | Text second
14 | Text 3
15 |
16 |
17 |
18 | Text one
19 | Text second
20 | Text 3
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | const styles = StyleSheet.create({
33 | container: {
34 | alignItems: 'center',
35 | },
36 | view: {
37 | width: '90%',
38 | borderWidth: 3,
39 | borderColor: 'black',
40 | marginBottom: 10,
41 | },
42 | title: {
43 | fontSize: 30,
44 | textAlign: 'center',
45 | borderWidth: 1,
46 | borderColor: 'red',
47 | margin: 20,
48 | },
49 | second: {
50 | height: 50,
51 | flexDirection: 'row',
52 | alignItems: 'center',
53 | justifyContent: 'space-evenly',
54 | },
55 | third: {
56 | height: 50,
57 | flexDirection: 'row',
58 | alignItems: 'center',
59 | justifyContent: 'space-around',
60 | },
61 | text: {
62 | backgroundColor: '#a0ffa0',
63 | borderWidth: 1,
64 | borderColor: 'red',
65 | padding: 3,
66 | },
67 | fourth: {
68 | flexDirection: 'row',
69 | width: 306,
70 | height: 206,
71 | alignItems: 'flex-start',
72 | },
73 | box: {
74 | width: 100,
75 | height: 100,
76 | },
77 | middle: {
78 | // marginTop: 100, // Other way
79 | alignSelf: 'flex-end',
80 | backgroundColor: '#00ff00',
81 | },
82 | });
83 |
84 | export default BoxScreen;
85 |
--------------------------------------------------------------------------------
/tracks/src/context/Auth.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { AsyncStorage } from 'react-native';
3 |
4 | import createDataContext from './createDataContext';
5 | import trackerAPI from '../api/tracker';
6 | import { navigate } from '../navigationRef';
7 |
8 | const AUTH_LOGIN = 'AUTH/LOGIN';
9 | const AUTH_LOGOUT = 'AUTH/LOGOUT';
10 | const AUTH_ERROR = 'AUTH/ERROR';
11 |
12 | const authSignup = dispatch => async (email, password) => {
13 | try {
14 | const response = await trackerAPI.post('/signup', { email, password });
15 | const { token } = response.data;
16 |
17 | await AsyncStorage.setItem('token', token);
18 |
19 | dispatch({ type: AUTH_LOGIN, token });
20 | navigate('TrackList');
21 | } catch (err) {
22 | // console.log(err.response.data);
23 | dispatch({
24 | type: AUTH_ERROR,
25 | message: 'Cannot sign up with that email address',
26 | });
27 | }
28 | };
29 |
30 | const authLogin = dispatch => async (email, password) => {
31 | try {
32 | const response = await trackerAPI.post('/login', { email, password });
33 | const { token } = response.data;
34 |
35 | await AsyncStorage.setItem('token', token);
36 |
37 | dispatch({ type: AUTH_LOGIN, token });
38 | navigate('TrackList');
39 | } catch (err) {
40 | // console.log(err.response.data);
41 | dispatch({ type: AUTH_ERROR, message: 'Unrecognised email or password' });
42 | }
43 | };
44 |
45 | const authTryLocalLogin = dispatch => async () => {
46 | const token = await AsyncStorage.getItem('token');
47 |
48 | if (token) {
49 | dispatch({ type: AUTH_LOGIN, token });
50 | navigate('TrackList');
51 | } else {
52 | navigate('Signup');
53 | }
54 | };
55 |
56 | const authLogout = dispatch => async () => {
57 | await AsyncStorage.removeItem('token');
58 | dispatch({ type: AUTH_LOGOUT });
59 | navigate('Login');
60 | };
61 |
62 | const authClearError = dispatch => () => {
63 | dispatch({ type: AUTH_ERROR, message: '' });
64 | };
65 |
66 | const authReducer = (auth, action) => {
67 | switch (action.type) {
68 | case AUTH_LOGIN:
69 | return { token: action.token, errorMessage: '' };
70 |
71 | case AUTH_LOGOUT:
72 | return { token: null, errorMessage: '' };
73 |
74 | case AUTH_ERROR:
75 | return { ...auth, errorMessage: action.message };
76 |
77 | default:
78 | return auth;
79 | }
80 | };
81 |
82 | const { Context: AuthContext, Provider: AuthProvider } = createDataContext(
83 | authReducer,
84 | { authSignup, authLogin, authTryLocalLogin, authLogout, authClearError },
85 | { token: null, errorMessage: '' }
86 | );
87 |
88 | const useAuth = () => {
89 | const context = useContext(AuthContext);
90 |
91 | if (context === undefined)
92 | throw new Error('useAuth must be used inside an AuthProvider');
93 |
94 | return context;
95 | };
96 |
97 | export { AuthProvider, useAuth };
98 |
--------------------------------------------------------------------------------
/blog/src/context/index.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import createDataContext from './createDataContext';
4 | import blogServer from '../api/posts';
5 |
6 | // Actions
7 | const LOAD_POSTS = 'LOAD_POSTS';
8 | const ADD_POST = 'ADD_POST';
9 | const UPDATE_POST = 'UPDATE_POST';
10 | const DELETE_POST = 'DELETE_POST';
11 |
12 | export const loadPosts = dispatch => {
13 | return async () => {
14 | try {
15 | const response = await blogServer.get('/posts');
16 |
17 | dispatch({ type: LOAD_POSTS, posts: response.data });
18 | } catch (err) {
19 | console.log('Error loading posts:', err);
20 | }
21 | };
22 | };
23 |
24 | export const addPost = dispatch => {
25 | return async (title, content, callback) => {
26 | try {
27 | const response = await blogServer.post('/posts', { title, content });
28 |
29 | dispatch({ type: ADD_POST, post: response.data });
30 | callback && callback();
31 | } catch (err) {
32 | console.log('Error adding post:', err);
33 | }
34 | };
35 | };
36 |
37 | export const updatePost = dispatch => {
38 | return async (id, title, content, callback) => {
39 | try {
40 | const response = await blogServer.put(`/posts/${id}`, { title, content });
41 |
42 | dispatch({ type: UPDATE_POST, post: response.data });
43 | callback && callback();
44 | } catch (err) {
45 | console.log('Error updating post:', err);
46 | }
47 | };
48 | };
49 |
50 | export const deletePost = dispatch => {
51 | return async (id, callback) => {
52 | try {
53 | await blogServer.delete(`/posts/${id}`);
54 | dispatch({ type: DELETE_POST, post: { id } });
55 | callback && callback();
56 | } catch (err) {
57 | console.log('Error deleting post:', err);
58 | }
59 | };
60 | };
61 |
62 | const blogReducer = (posts, action) => {
63 | let filteredPosts;
64 |
65 | switch (action.type) {
66 | case LOAD_POSTS:
67 | return action.posts;
68 |
69 | case ADD_POST:
70 | return [
71 | ...posts,
72 | { id: Math.floor(Math.random() * 9999999).toString(), ...action.post },
73 | ];
74 |
75 | case UPDATE_POST:
76 | filteredPosts = posts.filter(post => post.id !== action.post.id);
77 | return [...filteredPosts, action.post];
78 |
79 | case DELETE_POST:
80 | return posts.filter(post => post.id !== action.post.id);
81 |
82 | default:
83 | return posts;
84 | }
85 | };
86 |
87 | const { Context: BlogContext, Provider: BlogProvider } = createDataContext(
88 | blogReducer,
89 | { loadPosts, addPost, updatePost, deletePost },
90 | []
91 | );
92 |
93 | export const useBlog = () => {
94 | const context = useContext(BlogContext);
95 |
96 | if (context === undefined)
97 | throw new Error('useBlog() must be inside a BlogProvider block');
98 |
99 | return context;
100 | };
101 |
102 | export { BlogProvider };
103 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React-Native-Course
2 | Code from the Stephen Grider React Native course on
3 | [Udemy](https://www.udemy.com/course/the-complete-react-native-and-redux-course)
4 |
5 | ## PROBLEMS
6 |
7 | If you have any unexpected problems, for example where you have exactly the
8 | same code as Stephen but you have big red messages instead of an app.
9 |
10 | **TRY STOPPING THE SERVER AND RUNNING `npm start` AGAIN.**
11 |
12 | This is particularly true if you have messages like `Cannot load module "94"`
13 | or it disavows knowledge of a file that you've just created
14 | which is truly mystifying :-)
15 |
16 | Another thing is that you can't leave expo running during sleep
17 | or when you install new npm modules. It *will not work*, and you will get the
18 | mystifying errors.
19 |
20 | ## Progress
21 |
22 | Completed latest version of the course
23 |
24 | ## Differences from Stephen
25 |
26 | ### Actions
27 |
28 | I always create a set of 'types' for the action creators, this avoids any
29 | possibility of typos between action creators and reducers, e.g.
30 |
31 | ```
32 | dispatch({ type: 'LOAD_POSTS', posts });
33 | ...
34 |
35 | const postsReducer = (posts, action) => {
36 | switch (action.type) {
37 | case 'LAOD_POSTS': // Oops, should be 'LOAD_POSTS'
38 | ...
39 | ```
40 |
41 | ### Context
42 |
43 | I always create a custom hook for a context which returns the context, chacking
44 | that it's valid before returning it.
45 |
46 | ### First App
47 |
48 | * My rn-starter directory is more or less untouched from Stephen's zip file.
49 | I have started my first app in a directory called `first`.
50 |
51 | * I have styled everything much more in the first app.
52 |
53 | * I have added prop types to all the screens and components, because ESLint whines
54 | about them being missing :-)
55 |
56 | ### Food
57 |
58 | * I rarely, if ever, use the name `payload` for the data contained in a Redux /
59 | reducer action. The main exception to this is when using `redux-promise` which
60 | requires that the promised data name has to be `payload`.
61 |
62 | * My classes in the foodie app are generally `Restaurant...` where Stephen
63 | uses `Result(s)...`
64 |
65 | * The filtering in my foodie app is a lot more complicated than Stephen's.
66 | In the UK, the Yelp API frequently returns much less information, including
67 | an empty or missing `price` field.
68 |
69 | As a consequence of the previous, in addition to the lists with the three
70 | tiers of price, I have another list where the price range is unknown.
71 |
72 | Also, so many of the restaurants don't even have a single picture, so I have
73 | added a placeholder image to the restaurant lists, and have taken care of it
74 | on the restaurant detail page as well.
75 |
76 | ### Blog
77 |
78 | * Where Stephen passes the ID of a post to update or delete, I pass the complete
79 | post.
80 |
81 | * I have no clue why Stephen removes the call to dispatch for adding posts
82 | to the list in memory, necessitating a roundtrip to the server. This would be
83 | disastrous in a real application where there are potentially hundreds of posts
84 | to load from a remote server.
85 |
86 | Premature optimisation is to be avoided, but when the code is already there
87 | and tested, why would you stop using it?
88 |
89 | ### Track and Track Server
90 |
91 | * I always use the terms 'log in' and 'log out', to make a contrast with 'sign up',
92 | hence my login route is `/login` rather than `/signin` and my login and signup
93 | screens are `LoginScreen` and `SignupScreen` respectively.
94 |
95 | * I have checked the return value from `requestPermissionsAsync` so that I can
96 | detect the request being denied on iOS and not continue to attempt to get
97 | location data.
98 |
99 | * My LocationContext is called GeoContext because there were too many things called
100 | location(s). it also means that I can use `useGeo()` to return the context, leaving
101 | `useLocation()` free for Stephen's thing.
102 |
103 | * Stephen is not going the right way about recording. The context
104 | already knows whether recording is in progress, it does not need to be
105 | communicated from outside, where it's being retrieved from the context anyway...
106 |
107 | His method actually breaks the recording process so badly that he has to spend
108 | multiple videos fixing the problem that he created.
109 |
110 | * My `_mockLocation` makes a wigglier line than Stephen's by not adding to both
111 | latitude and longitude every time. I also slowed the update rate down to every 2s.
112 |
113 | ### Git client
114 |
115 | I have used Git at the command-line for more than 10 years. Over that time, I have tried
116 | many different graphical shells for Git, without finding one that was easier
117 | and nicer to use than the command-line (in my view).
118 |
119 | I have now found that [GitKraken](https://www.gitkraken.com) is an excellent
120 | Git shell and would advise using it to everyone.
121 |
122 | ### Questions
123 |
124 | If you have any questions about this repository, or any others of mine, please
125 | don't hesitate to contact me.
126 |
127 |
--------------------------------------------------------------------------------
/blog-server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blog-server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@polka/url": {
8 | "version": "1.0.0-next.25",
9 | "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz",
10 | "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ=="
11 | },
12 | "@sindresorhus/is": {
13 | "version": "4.6.0",
14 | "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
15 | "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="
16 | },
17 | "@szmarczak/http-timer": {
18 | "version": "4.0.6",
19 | "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
20 | "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
21 | "requires": {
22 | "defer-to-connect": "^2.0.0"
23 | }
24 | },
25 | "@tinyhttp/accepts": {
26 | "version": "2.2.2",
27 | "resolved": "https://registry.npmjs.org/@tinyhttp/accepts/-/accepts-2.2.2.tgz",
28 | "integrity": "sha512-DmngnwOaPgNUGgTpX1UdlNrXaGG6k5rHFzslcYlvSQIg7s0PI6bF86U0fYq3q+yhJhKbnwzFez0wU1lAP+bKvA==",
29 | "requires": {
30 | "mime": "4.0.1",
31 | "negotiator": "^0.6.3"
32 | }
33 | },
34 | "@tinyhttp/app": {
35 | "version": "2.3.1",
36 | "resolved": "https://registry.npmjs.org/@tinyhttp/app/-/app-2.3.1.tgz",
37 | "integrity": "sha512-46sHcWTERHAHaBWRypXLH+qRXoYq1j9GfXr/TR2HIsuXWbXlirD77JbwabbTuGHFb5Cg8cisWEa+pTU7lp9t5A==",
38 | "requires": {
39 | "@tinyhttp/cookie": "2.1.1",
40 | "@tinyhttp/proxy-addr": "2.2.0",
41 | "@tinyhttp/req": "2.2.3",
42 | "@tinyhttp/res": "2.2.3",
43 | "@tinyhttp/router": "2.2.2",
44 | "header-range-parser": "1.1.3",
45 | "regexparam": "^2.0.2"
46 | }
47 | },
48 | "@tinyhttp/content-disposition": {
49 | "version": "2.2.1",
50 | "resolved": "https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.1.tgz",
51 | "integrity": "sha512-PQ5IWdOn7arScqTV+usIDJvwbanoAXtaopzgxjMS9y7TFwLSIelCblihRBEVIPIkIpsdhSJFH3RF+Daosyj+Aw=="
52 | },
53 | "@tinyhttp/content-type": {
54 | "version": "0.1.4",
55 | "resolved": "https://registry.npmjs.org/@tinyhttp/content-type/-/content-type-0.1.4.tgz",
56 | "integrity": "sha512-dl6f3SHIJPYbhsW1oXdrqOmLSQF/Ctlv3JnNfXAE22kIP7FosqJHxkz/qj2gv465prG8ODKH5KEyhBkvwrueKQ=="
57 | },
58 | "@tinyhttp/cookie": {
59 | "version": "2.1.1",
60 | "resolved": "https://registry.npmjs.org/@tinyhttp/cookie/-/cookie-2.1.1.tgz",
61 | "integrity": "sha512-h/kL9jY0e0Dvad+/QU3efKZww0aTvZJslaHj3JTPmIPC9Oan9+kYqmh3M6L5JUQRuTJYFK2nzgL2iJtH2S+6dA=="
62 | },
63 | "@tinyhttp/cookie-signature": {
64 | "version": "2.1.1",
65 | "resolved": "https://registry.npmjs.org/@tinyhttp/cookie-signature/-/cookie-signature-2.1.1.tgz",
66 | "integrity": "sha512-VDsSMY5OJfQJIAtUgeQYhqMPSZptehFSfvEEtxr+4nldPA8IImlp3QVcOVuK985g4AFR4Hl1sCbWCXoqBnVWnw=="
67 | },
68 | "@tinyhttp/cors": {
69 | "version": "2.0.1",
70 | "resolved": "https://registry.npmjs.org/@tinyhttp/cors/-/cors-2.0.1.tgz",
71 | "integrity": "sha512-qrmo6WJuaiCzKWagv2yA/kw6hIISfF/hOqPWwmI6w0o8apeTMmRN3DoCFvQ/wNVuWVdU5J4KU7OX8aaSOEq51A==",
72 | "requires": {
73 | "@tinyhttp/vary": "^0.1.3"
74 | }
75 | },
76 | "@tinyhttp/encode-url": {
77 | "version": "2.1.1",
78 | "resolved": "https://registry.npmjs.org/@tinyhttp/encode-url/-/encode-url-2.1.1.tgz",
79 | "integrity": "sha512-AhY+JqdZ56qV77tzrBm0qThXORbsVjs/IOPgGCS7x/wWnsa/Bx30zDUU/jPAUcSzNOzt860x9fhdGpzdqbUeUw=="
80 | },
81 | "@tinyhttp/etag": {
82 | "version": "2.1.2",
83 | "resolved": "https://registry.npmjs.org/@tinyhttp/etag/-/etag-2.1.2.tgz",
84 | "integrity": "sha512-j80fPKimGqdmMh6962y+BtQsnYPVCzZfJw0HXjyH70VaJBHLKGF+iYhcKqzI3yef6QBNa8DKIPsbEYpuwApXTw=="
85 | },
86 | "@tinyhttp/forwarded": {
87 | "version": "2.1.1",
88 | "resolved": "https://registry.npmjs.org/@tinyhttp/forwarded/-/forwarded-2.1.1.tgz",
89 | "integrity": "sha512-nO3kq0R1LRl2+CAMlnggm22zE6sT8gfvGbNvSitV6F9eaUSurHP0A8YZFMihSkugHxK+uIegh1TKrqgD8+lyGQ=="
90 | },
91 | "@tinyhttp/proxy-addr": {
92 | "version": "2.2.0",
93 | "resolved": "https://registry.npmjs.org/@tinyhttp/proxy-addr/-/proxy-addr-2.2.0.tgz",
94 | "integrity": "sha512-WM/PPL9xNvrs7/8Om5nhKbke5FHrP3EfjOOR+wBnjgESfibqn0K7wdUTnzSLp1lBmemr88os1XvzwymSgaibyA==",
95 | "requires": {
96 | "@tinyhttp/forwarded": "2.1.1",
97 | "ipaddr.js": "^2.2.0"
98 | }
99 | },
100 | "@tinyhttp/req": {
101 | "version": "2.2.3",
102 | "resolved": "https://registry.npmjs.org/@tinyhttp/req/-/req-2.2.3.tgz",
103 | "integrity": "sha512-HtIa4Gaa8QFTlmsvoif/B7yMK5H0WBUegH2kKW6scNwOpFXyxEk+VsctrIVgORrP5lybXAIRXlRhGuBBAMlVhw==",
104 | "requires": {
105 | "@tinyhttp/accepts": "2.2.2",
106 | "@tinyhttp/type-is": "2.2.3",
107 | "@tinyhttp/url": "2.1.1",
108 | "header-range-parser": "^1.1.3"
109 | }
110 | },
111 | "@tinyhttp/res": {
112 | "version": "2.2.3",
113 | "resolved": "https://registry.npmjs.org/@tinyhttp/res/-/res-2.2.3.tgz",
114 | "integrity": "sha512-PGl88OOdmMcOuKZaTbhGKAWPoJJf3+EfKIad8ydzjdenVjrTZZjIYJtmwYiUBeEice+YkOCO67qCIekVO5mHlw==",
115 | "requires": {
116 | "@tinyhttp/content-disposition": "2.2.1",
117 | "@tinyhttp/cookie": "2.1.1",
118 | "@tinyhttp/cookie-signature": "2.1.1",
119 | "@tinyhttp/encode-url": "2.1.1",
120 | "@tinyhttp/req": "2.2.3",
121 | "@tinyhttp/send": "2.2.2",
122 | "@tinyhttp/vary": "^0.1.3",
123 | "es-escape-html": "^0.1.1",
124 | "mime": "4.0.0-beta.1"
125 | },
126 | "dependencies": {
127 | "mime": {
128 | "version": "4.0.0-beta.1",
129 | "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.0-beta.1.tgz",
130 | "integrity": "sha512-8/p99P1RV17prytee/A6D+8shNqdDzyvGJ/CVfiuXwh4cTsv3P3qGyaYSx2hdqnqbKKqYUfTC5zAjCtcd1BShw=="
131 | }
132 | }
133 | },
134 | "@tinyhttp/router": {
135 | "version": "2.2.2",
136 | "resolved": "https://registry.npmjs.org/@tinyhttp/router/-/router-2.2.2.tgz",
137 | "integrity": "sha512-i+1ouhPyTqcuJuOsKqmo7i+YD++0RF2lQLhBpcTnsaegD2gTEa3xW2Pcz7spYQGo7K8PQYtOrL7m9b14+BEXqg=="
138 | },
139 | "@tinyhttp/send": {
140 | "version": "2.2.2",
141 | "resolved": "https://registry.npmjs.org/@tinyhttp/send/-/send-2.2.2.tgz",
142 | "integrity": "sha512-TZkGy9EdGk+vwYWQnjArQftaXAUIgp/fFlgaxlpamsCZKy7o+CNJ75xty4H3SaY3ZPgN47wv8rnJ50rDRQdFFQ==",
143 | "requires": {
144 | "@tinyhttp/content-type": "^0.1.4",
145 | "@tinyhttp/etag": "2.1.2",
146 | "mime": "4.0.0-beta.1"
147 | },
148 | "dependencies": {
149 | "mime": {
150 | "version": "4.0.0-beta.1",
151 | "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.0-beta.1.tgz",
152 | "integrity": "sha512-8/p99P1RV17prytee/A6D+8shNqdDzyvGJ/CVfiuXwh4cTsv3P3qGyaYSx2hdqnqbKKqYUfTC5zAjCtcd1BShw=="
153 | }
154 | }
155 | },
156 | "@tinyhttp/type-is": {
157 | "version": "2.2.3",
158 | "resolved": "https://registry.npmjs.org/@tinyhttp/type-is/-/type-is-2.2.3.tgz",
159 | "integrity": "sha512-RsZ4+or5xI+wrTlrd+/cLZELoJDMd1HSp+1P23VOZSu1xPAsO1XLf1FgluO8GbEW9Ll/l2yC7mO6diKzjc06HA==",
160 | "requires": {
161 | "@tinyhttp/content-type": "^0.1.4",
162 | "mime": "4.0.1"
163 | }
164 | },
165 | "@tinyhttp/url": {
166 | "version": "2.1.1",
167 | "resolved": "https://registry.npmjs.org/@tinyhttp/url/-/url-2.1.1.tgz",
168 | "integrity": "sha512-POJeq2GQ5jI7Zrdmj22JqOijB5/GeX+LEX7DUdml1hUnGbJOTWDx7zf2b5cCERj7RoXL67zTgyzVblBJC+NJWg=="
169 | },
170 | "@tinyhttp/vary": {
171 | "version": "0.1.3",
172 | "resolved": "https://registry.npmjs.org/@tinyhttp/vary/-/vary-0.1.3.tgz",
173 | "integrity": "sha512-SoL83sQXAGiHN1jm2VwLUWQSQeDAAl1ywOm6T0b0Cg1CZhVsjoiZadmjhxF6FHCCY7OHHVaLnTgSMxTPIDLxMg=="
174 | },
175 | "@types/cacheable-request": {
176 | "version": "6.0.3",
177 | "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
178 | "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
179 | "requires": {
180 | "@types/http-cache-semantics": "*",
181 | "@types/keyv": "^3.1.4",
182 | "@types/node": "*",
183 | "@types/responselike": "^1.0.0"
184 | }
185 | },
186 | "@types/http-cache-semantics": {
187 | "version": "4.0.1",
188 | "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
189 | "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
190 | },
191 | "@types/keyv": {
192 | "version": "3.1.4",
193 | "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
194 | "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
195 | "requires": {
196 | "@types/node": "*"
197 | }
198 | },
199 | "@types/node": {
200 | "version": "20.4.1",
201 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.1.tgz",
202 | "integrity": "sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg=="
203 | },
204 | "@types/responselike": {
205 | "version": "1.0.0",
206 | "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
207 | "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
208 | "requires": {
209 | "@types/node": "*"
210 | }
211 | },
212 | "@types/yauzl": {
213 | "version": "2.10.0",
214 | "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
215 | "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==",
216 | "optional": true,
217 | "requires": {
218 | "@types/node": "*"
219 | }
220 | },
221 | "anymatch": {
222 | "version": "3.1.3",
223 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
224 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
225 | "requires": {
226 | "normalize-path": "^3.0.0",
227 | "picomatch": "^2.0.4"
228 | }
229 | },
230 | "binary-extensions": {
231 | "version": "2.3.0",
232 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
233 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="
234 | },
235 | "braces": {
236 | "version": "3.0.3",
237 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
238 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
239 | "requires": {
240 | "fill-range": "^7.1.1"
241 | }
242 | },
243 | "buffer-crc32": {
244 | "version": "0.2.13",
245 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
246 | "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="
247 | },
248 | "cacheable-lookup": {
249 | "version": "5.0.4",
250 | "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
251 | "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="
252 | },
253 | "cacheable-request": {
254 | "version": "7.0.4",
255 | "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
256 | "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
257 | "requires": {
258 | "clone-response": "^1.0.2",
259 | "get-stream": "^5.1.0",
260 | "http-cache-semantics": "^4.0.0",
261 | "keyv": "^4.0.0",
262 | "lowercase-keys": "^2.0.0",
263 | "normalize-url": "^6.0.1",
264 | "responselike": "^2.0.0"
265 | }
266 | },
267 | "chalk": {
268 | "version": "5.3.0",
269 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
270 | "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="
271 | },
272 | "chokidar": {
273 | "version": "3.6.0",
274 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
275 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
276 | "requires": {
277 | "anymatch": "~3.1.2",
278 | "braces": "~3.0.2",
279 | "fsevents": "~2.3.2",
280 | "glob-parent": "~5.1.2",
281 | "is-binary-path": "~2.1.0",
282 | "is-glob": "~4.0.1",
283 | "normalize-path": "~3.0.0",
284 | "readdirp": "~3.6.0"
285 | }
286 | },
287 | "clone-response": {
288 | "version": "1.0.3",
289 | "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
290 | "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
291 | "requires": {
292 | "mimic-response": "^1.0.0"
293 | }
294 | },
295 | "decompress-response": {
296 | "version": "6.0.0",
297 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
298 | "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
299 | "requires": {
300 | "mimic-response": "^3.1.0"
301 | },
302 | "dependencies": {
303 | "mimic-response": {
304 | "version": "3.1.0",
305 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
306 | "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
307 | }
308 | }
309 | },
310 | "defer-to-connect": {
311 | "version": "2.0.1",
312 | "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
313 | "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
314 | },
315 | "dot-prop": {
316 | "version": "9.0.0",
317 | "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz",
318 | "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==",
319 | "requires": {
320 | "type-fest": "^4.18.2"
321 | }
322 | },
323 | "end-of-stream": {
324 | "version": "1.4.4",
325 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
326 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
327 | "requires": {
328 | "once": "^1.4.0"
329 | }
330 | },
331 | "es-escape-html": {
332 | "version": "0.1.1",
333 | "resolved": "https://registry.npmjs.org/es-escape-html/-/es-escape-html-0.1.1.tgz",
334 | "integrity": "sha512-yUx1o+8RsG7UlszmYPtks+dm6Lho2m8lgHMOsLJQsFI0R8XwUJwiMhM1M4E/S8QLeGyf6MkDV/pWgjQ0tdTSyQ=="
335 | },
336 | "eta": {
337 | "version": "3.5.0",
338 | "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz",
339 | "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug=="
340 | },
341 | "extract-zip": {
342 | "version": "2.0.1",
343 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
344 | "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
345 | "requires": {
346 | "@types/yauzl": "^2.9.1",
347 | "debug": "^4.1.1",
348 | "get-stream": "^5.1.0",
349 | "yauzl": "^2.10.0"
350 | },
351 | "dependencies": {
352 | "debug": {
353 | "version": "4.3.4",
354 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
355 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
356 | "requires": {
357 | "ms": "2.1.2"
358 | }
359 | },
360 | "ms": {
361 | "version": "2.1.2",
362 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
363 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
364 | }
365 | }
366 | },
367 | "fd-slicer": {
368 | "version": "1.1.0",
369 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
370 | "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
371 | "requires": {
372 | "pend": "~1.2.0"
373 | }
374 | },
375 | "fill-range": {
376 | "version": "7.1.1",
377 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
378 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
379 | "requires": {
380 | "to-regex-range": "^5.0.1"
381 | }
382 | },
383 | "fsevents": {
384 | "version": "2.3.3",
385 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
386 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
387 | "optional": true
388 | },
389 | "get-stream": {
390 | "version": "5.2.0",
391 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
392 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
393 | "requires": {
394 | "pump": "^3.0.0"
395 | }
396 | },
397 | "glob-parent": {
398 | "version": "5.1.2",
399 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
400 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
401 | "requires": {
402 | "is-glob": "^4.0.1"
403 | }
404 | },
405 | "got": {
406 | "version": "11.8.6",
407 | "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
408 | "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
409 | "requires": {
410 | "@sindresorhus/is": "^4.0.0",
411 | "@szmarczak/http-timer": "^4.0.5",
412 | "@types/cacheable-request": "^6.0.1",
413 | "@types/responselike": "^1.0.0",
414 | "cacheable-lookup": "^5.0.3",
415 | "cacheable-request": "^7.0.2",
416 | "decompress-response": "^6.0.0",
417 | "http2-wrapper": "^1.0.0-beta.5.2",
418 | "lowercase-keys": "^2.0.0",
419 | "p-cancelable": "^2.0.0",
420 | "responselike": "^2.0.0"
421 | }
422 | },
423 | "header-range-parser": {
424 | "version": "1.1.3",
425 | "resolved": "https://registry.npmjs.org/header-range-parser/-/header-range-parser-1.1.3.tgz",
426 | "integrity": "sha512-B9zCFt3jH8g09LR1vHL4pcAn8yMEtlSlOUdQemzHMRKMImNIhhszdeosYFfNW0WXKQtXIlWB+O4owHJKvEJYaA=="
427 | },
428 | "hpagent": {
429 | "version": "0.1.2",
430 | "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-0.1.2.tgz",
431 | "integrity": "sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==",
432 | "optional": true
433 | },
434 | "http-cache-semantics": {
435 | "version": "4.1.1",
436 | "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
437 | "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
438 | },
439 | "http2-wrapper": {
440 | "version": "1.0.3",
441 | "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
442 | "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
443 | "requires": {
444 | "quick-lru": "^5.1.1",
445 | "resolve-alpn": "^1.0.0"
446 | }
447 | },
448 | "inflection": {
449 | "version": "3.0.0",
450 | "resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.0.tgz",
451 | "integrity": "sha512-1zEJU1l19SgJlmwqsEyFTbScw/tkMHFenUo//Y0i+XEP83gDFdMvPizAD/WGcE+l1ku12PcTVHQhO6g5E0UCMw=="
452 | },
453 | "ipaddr.js": {
454 | "version": "2.2.0",
455 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
456 | "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="
457 | },
458 | "is-binary-path": {
459 | "version": "2.1.0",
460 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
461 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
462 | "requires": {
463 | "binary-extensions": "^2.0.0"
464 | }
465 | },
466 | "is-extglob": {
467 | "version": "2.1.1",
468 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
469 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
470 | },
471 | "is-glob": {
472 | "version": "4.0.3",
473 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
474 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
475 | "requires": {
476 | "is-extglob": "^2.1.1"
477 | }
478 | },
479 | "is-number": {
480 | "version": "7.0.0",
481 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
482 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
483 | },
484 | "json-buffer": {
485 | "version": "3.0.1",
486 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
487 | "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
488 | },
489 | "json-server": {
490 | "version": "1.0.0-beta.2",
491 | "resolved": "https://registry.npmjs.org/json-server/-/json-server-1.0.0-beta.2.tgz",
492 | "integrity": "sha512-ZzBM2jkIeBTYIEW5c8DHWJacRqC+ah2u623SV75JYeZv9eJQ0Rv2rSkc2w3f02xykIDlyQ9aitAUIZnNgYB5BQ==",
493 | "requires": {
494 | "@tinyhttp/app": "^2.2.3",
495 | "@tinyhttp/cors": "^2.0.0",
496 | "chalk": "^5.3.0",
497 | "chokidar": "^3.6.0",
498 | "dot-prop": "^9.0.0",
499 | "eta": "^3.4.0",
500 | "inflection": "^3.0.0",
501 | "json5": "^2.2.3",
502 | "lowdb": "^7.0.1",
503 | "milliparsec": "^2.3.0",
504 | "sirv": "^2.0.4",
505 | "sort-on": "^6.0.0"
506 | }
507 | },
508 | "json5": {
509 | "version": "2.2.3",
510 | "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
511 | "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
512 | },
513 | "keyv": {
514 | "version": "4.5.2",
515 | "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz",
516 | "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==",
517 | "requires": {
518 | "json-buffer": "3.0.1"
519 | }
520 | },
521 | "lodash.clonedeep": {
522 | "version": "4.5.0",
523 | "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
524 | "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
525 | },
526 | "lowdb": {
527 | "version": "7.0.1",
528 | "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz",
529 | "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==",
530 | "requires": {
531 | "steno": "^4.0.2"
532 | }
533 | },
534 | "lowercase-keys": {
535 | "version": "2.0.0",
536 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
537 | "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
538 | },
539 | "milliparsec": {
540 | "version": "2.3.0",
541 | "resolved": "https://registry.npmjs.org/milliparsec/-/milliparsec-2.3.0.tgz",
542 | "integrity": "sha512-b+6KYJw+DwQjk24qCUuq+lZvRXDpXJ02qsllKgKaDurHpQ0v7D5op9VAkdYM/pXRhFeh7uLYHmnwFnYvdXGa3A=="
543 | },
544 | "mime": {
545 | "version": "4.0.1",
546 | "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.1.tgz",
547 | "integrity": "sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA=="
548 | },
549 | "mimic-response": {
550 | "version": "1.0.1",
551 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
552 | "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
553 | },
554 | "mrmime": {
555 | "version": "2.0.0",
556 | "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
557 | "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw=="
558 | },
559 | "negotiator": {
560 | "version": "0.6.3",
561 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
562 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
563 | },
564 | "ngrok": {
565 | "version": "5.0.0-beta.2",
566 | "resolved": "https://registry.npmjs.org/ngrok/-/ngrok-5.0.0-beta.2.tgz",
567 | "integrity": "sha512-UzsyGiJ4yTTQLCQD11k1DQaMwq2/SsztBg2b34zAqcyjS25qjDpogMKPaCKHwe/APRTHeel3iDXcVctk5CNaCQ==",
568 | "requires": {
569 | "extract-zip": "^2.0.1",
570 | "got": "^11.8.5",
571 | "hpagent": "^0.1.2",
572 | "lodash.clonedeep": "^4.5.0",
573 | "uuid": "^7.0.0 || ^8.0.0",
574 | "yaml": "^2.2.2"
575 | }
576 | },
577 | "normalize-path": {
578 | "version": "3.0.0",
579 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
580 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
581 | },
582 | "normalize-url": {
583 | "version": "6.1.0",
584 | "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
585 | "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
586 | },
587 | "once": {
588 | "version": "1.4.0",
589 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
590 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
591 | "requires": {
592 | "wrappy": "1"
593 | }
594 | },
595 | "p-cancelable": {
596 | "version": "2.1.1",
597 | "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
598 | "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="
599 | },
600 | "pend": {
601 | "version": "1.2.0",
602 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
603 | "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
604 | },
605 | "picomatch": {
606 | "version": "2.3.1",
607 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
608 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
609 | },
610 | "pump": {
611 | "version": "3.0.0",
612 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
613 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
614 | "requires": {
615 | "end-of-stream": "^1.1.0",
616 | "once": "^1.3.1"
617 | }
618 | },
619 | "quick-lru": {
620 | "version": "5.1.1",
621 | "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
622 | "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
623 | },
624 | "readdirp": {
625 | "version": "3.6.0",
626 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
627 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
628 | "requires": {
629 | "picomatch": "^2.2.1"
630 | }
631 | },
632 | "regexparam": {
633 | "version": "2.0.2",
634 | "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz",
635 | "integrity": "sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w=="
636 | },
637 | "resolve-alpn": {
638 | "version": "1.2.1",
639 | "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
640 | "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
641 | },
642 | "responselike": {
643 | "version": "2.0.1",
644 | "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
645 | "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
646 | "requires": {
647 | "lowercase-keys": "^2.0.0"
648 | }
649 | },
650 | "sirv": {
651 | "version": "2.0.4",
652 | "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
653 | "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
654 | "requires": {
655 | "@polka/url": "^1.0.0-next.24",
656 | "mrmime": "^2.0.0",
657 | "totalist": "^3.0.0"
658 | }
659 | },
660 | "sort-on": {
661 | "version": "6.1.0",
662 | "resolved": "https://registry.npmjs.org/sort-on/-/sort-on-6.1.0.tgz",
663 | "integrity": "sha512-WTECP0nYNWO1n2g5bpsV0yZN9cBmZsF8ThHFbOqVN0HBFRoaQZLLEMvMmJlKHNPYQeVngeI5+jJzIfFqOIo1OA==",
664 | "requires": {
665 | "dot-prop": "^9.0.0"
666 | }
667 | },
668 | "steno": {
669 | "version": "4.0.2",
670 | "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz",
671 | "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A=="
672 | },
673 | "to-regex-range": {
674 | "version": "5.0.1",
675 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
676 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
677 | "requires": {
678 | "is-number": "^7.0.0"
679 | }
680 | },
681 | "totalist": {
682 | "version": "3.0.1",
683 | "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
684 | "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="
685 | },
686 | "type-fest": {
687 | "version": "4.26.1",
688 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz",
689 | "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg=="
690 | },
691 | "uuid": {
692 | "version": "8.3.2",
693 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
694 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
695 | },
696 | "wrappy": {
697 | "version": "1.0.2",
698 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
699 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
700 | },
701 | "yaml": {
702 | "version": "2.3.1",
703 | "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
704 | "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ=="
705 | },
706 | "yauzl": {
707 | "version": "2.10.0",
708 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
709 | "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
710 | "requires": {
711 | "buffer-crc32": "~0.2.3",
712 | "fd-slicer": "~1.1.0"
713 | }
714 | }
715 | }
716 | }
717 |
--------------------------------------------------------------------------------