├── .github
├── FUNDING.yml
└── workflows
│ ├── expo-build-test.yml
│ ├── native-build-test.yml
│ └── web-build-test.yml
├── .gitignore
├── OURS-EXPO
├── .github
│ └── workflows
│ │ └── publish.yml
├── README.md
├── assets
│ ├── icon.png
│ └── splash.png
├── documentation
│ ├── expo-demo.jpg
│ └── rnsk-logo.jpg
└── src
│ └── index.js
├── OURS-NATIVE
├── App.js
├── README.md
├── android
│ └── app
│ │ └── src
│ │ └── main
│ │ └── res
│ │ ├── drawable-xxhdpi
│ │ └── launch_screen.png
│ │ └── layout
│ │ └── launch_screen.xml
├── documentation
│ ├── deploy.md
│ ├── faqs.md
│ ├── file-structure.md
│ └── rnsk-logo.jpg
├── fastlane
│ ├── Appfile
│ ├── Fastfile
│ ├── README.md
│ ├── metadata
│ │ └── android
│ │ │ └── en-GB
│ │ │ ├── full_description.txt
│ │ │ ├── short_description.txt
│ │ │ ├── title.txt
│ │ │ └── video.txt
│ └── update-app-version.sh
├── index.js
├── ios
│ ├── Base.lproj
│ │ └── LaunchScreen.xib
│ └── Images.xcassets
│ │ ├── AppIcon.appiconset
│ │ ├── 120.png
│ │ ├── 180.png
│ │ └── Contents.json
│ │ └── LaunchScreen.imageset
│ │ ├── Contents.json
│ │ ├── LaunchImage.png
│ │ ├── LaunchImage2x.png
│ │ └── LaunchImage3x.png
└── src
│ ├── components
│ ├── About.js
│ ├── Articles
│ │ ├── Form.js
│ │ ├── List.js
│ │ └── Single.js
│ └── UI
│ │ ├── Error.js
│ │ ├── Header.js
│ │ ├── Loading.js
│ │ ├── Messages.js
│ │ ├── Spacer.js
│ │ └── index.js
│ ├── constants
│ ├── config.js
│ └── navigation.js
│ ├── index.js
│ ├── lib
│ └── api.js
│ ├── routes
│ └── index.js
│ ├── store
│ └── index.js
│ └── tests
│ ├── __mocks__
│ └── @react-native-community
│ │ └── async-storage.js
│ └── components
│ ├── Articles
│ ├── List.test.js
│ ├── Single.test.js
│ └── __snapshots__
│ │ ├── List.test.js.snap
│ │ └── Single.test.js.snap
│ └── UI
│ ├── Error.test.js
│ ├── Header.test.js
│ ├── Loading.test.js
│ ├── Messages.test.js
│ ├── Spacer.test.js
│ └── __snapshots__
│ ├── Error.test.js.snap
│ ├── Header.test.js.snap
│ ├── Loading.test.js.snap
│ ├── Messages.test.js.snap
│ └── Spacer.test.js.snap
├── OURS-WEB
├── .github
│ └── workflows
│ │ └── publish.yml
├── README.md
├── documentation
│ ├── rsk-logo.jpg
│ └── web-demo.jpg
├── public
│ ├── 404.html
│ └── CNAME
└── src
│ ├── assets
│ ├── images
│ │ └── app-icon.png
│ └── styles
│ │ ├── _bootstrap.scss
│ │ ├── components
│ │ ├── _footer.scss
│ │ ├── _forms.scss
│ │ ├── _header.scss
│ │ ├── _mobile-tab-bar.scss
│ │ └── _tables.scss
│ │ └── style.scss
│ ├── components
│ ├── About.js
│ ├── Articles
│ │ ├── Form.js
│ │ ├── List.js
│ │ └── Single.js
│ ├── Templates
│ │ ├── Dashboard.js
│ │ └── Nothing.js
│ └── UI
│ │ ├── Error.js
│ │ ├── Footer.js
│ │ ├── Header.js
│ │ ├── Loading.js
│ │ ├── MobileTabBar.js
│ │ ├── Notice.js
│ │ ├── PageTitle.js
│ │ ├── TablePagination.js
│ │ └── index.js
│ ├── constants
│ └── config.js
│ ├── index.js
│ ├── lib
│ ├── api.js
│ ├── cookies.js
│ ├── jwt.js
│ └── service-worker.js
│ ├── routes
│ ├── PrivateRoute.js
│ ├── Route.js
│ └── index.js
│ ├── setupTests.js
│ ├── store
│ └── index.js
│ └── tests
│ ├── components
│ ├── Articles
│ │ ├── Form.test.js
│ │ ├── List.test.js
│ │ ├── Single.test.js
│ │ └── __snapshots__
│ │ │ ├── Form.test.js.snap
│ │ │ ├── List.test.js.snap
│ │ │ └── Single.test.js.snap
│ └── UI
│ │ ├── Error.test.js
│ │ ├── Footer.test.js
│ │ ├── Header.test.js
│ │ ├── Loading.test.js
│ │ ├── MobileTabBar.test.js
│ │ ├── PageTitle.test.js
│ │ ├── TablePagination.test.js
│ │ └── __snapshots__
│ │ ├── Error.test.js.snap
│ │ ├── Footer.test.js.snap
│ │ ├── Header.test.js.snap
│ │ ├── Loading.test.js.snap
│ │ ├── MobileTabBar.test.js.snap
│ │ ├── PageTitle.test.js.snap
│ │ └── TablePagination.test.js.snap
│ ├── constants
│ └── config.test.js
│ └── lib
│ └── jwt.test.js
├── OURS
├── .editorconfig
├── .eslintrc.js
├── .github
│ ├── FUNDING.yml
│ └── workflows
│ │ └── test.yml
├── .prettierrc.js
├── ISSUE_TEMPLATE.md
├── LICENSE
├── documentation
│ ├── contributing.md
│ ├── file-structure.md
│ └── testing.md
└── src
│ ├── constants
│ └── messages.js
│ ├── containers
│ ├── Articles
│ │ ├── Form.js
│ │ ├── List.js
│ │ └── Single.js
│ └── index.js
│ ├── images
│ ├── app-icon.png
│ └── launch.png
│ ├── lib
│ ├── format-error-messages.js
│ ├── images.js
│ ├── pagination.js
│ └── string.js
│ ├── models
│ ├── articles.js
│ └── index.js
│ ├── store
│ └── articles.js
│ └── tests
│ ├── __mocks__
│ ├── react-native-gesture-handler.js
│ ├── react-native-reanimated.js
│ ├── react-native-tab-view.js
│ ├── react-navigation-stack.js
│ └── react-redux.js
│ ├── lib
│ ├── format-error-messages.test.js
│ ├── images.test.js
│ ├── pagination.test.js
│ └── string.test.js
│ └── models
│ └── articles.test.js
├── README.md
└── build.sh
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: mcnamee
2 |
--------------------------------------------------------------------------------
/.github/workflows/expo-build-test.yml:
--------------------------------------------------------------------------------
1 | name: Expo Build + Test
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * MON'
6 | push:
7 | branches:
8 | - master
9 | pull_request:
10 | branches:
11 | - master
12 |
13 | jobs:
14 | build:
15 | runs-on: macOS-latest
16 | strategy:
17 | matrix:
18 | node-version: [14.x]
19 |
20 | steps:
21 | - uses: actions/checkout@v1
22 | - name: Use Node.js ${{ matrix.node-version }}
23 | uses: actions/setup-node@v1
24 | with:
25 | node-version: ${{ matrix.node-version }}
26 | - name: Get Commit Message
27 | run: |
28 | echo "::set-env name=COMMIT_MESSAGE::$(git log -1 --pretty=format:%s)"
29 | - name: Build a fresh Expo App
30 | run: |
31 | bash build.sh --name=ReactNativeExpoStarterKit --type=3
32 | - name: Lint
33 | run: |
34 | ./node_modules/.bin/eslint "src/**/*.js"
35 | - name: Jest Tests
36 | run: |
37 | ./node_modules/.bin/jest --silent -u
38 | env:
39 | CI: true
40 | - name: Commit files to React Native (Expo) Starter Kit repo
41 | run: |
42 | rm -rf .git
43 | git clone https://github.com/mcnamee/react-native-expo-starter-kit.git TEMP
44 | mv TEMP/.git .git
45 | rm -rf TEMP
46 | git config --local user.email "${{ secrets.GH_EMAIL }}"
47 | git config --local user.name "mcnamee/react-native-boilerplate-builder"
48 | git add -A
49 | git commit -m "$COMMIT_MESSAGE (RNBB Bot)" -a
50 | - name: Push files to React Native Starter Kit repo
51 | uses: ad-m/github-push-action@master
52 | with:
53 | github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
54 | repository: mcnamee/react-native-expo-starter-kit
55 | branch: master
56 |
--------------------------------------------------------------------------------
/.github/workflows/native-build-test.yml:
--------------------------------------------------------------------------------
1 | name: Native Build + Test
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * MON'
6 | push:
7 | branches:
8 | - master
9 | pull_request:
10 | branches:
11 | - master
12 |
13 | jobs:
14 | build:
15 | runs-on: macOS-latest
16 | strategy:
17 | matrix:
18 | node-version: [14.x]
19 |
20 | steps:
21 | - uses: actions/checkout@v1
22 | - name: Use Node.js ${{ matrix.node-version }}
23 | uses: actions/setup-node@v1
24 | with:
25 | node-version: ${{ matrix.node-version }}
26 | - name: Build a fresh React Native App
27 | run: |
28 | bash build.sh --name=ReactNativeStarterKit --type=2
29 | - name: Lint
30 | run: |
31 | ./node_modules/.bin/eslint "src/**/*.js"
32 | - name: Jest Tests
33 | run: |
34 | ./node_modules/.bin/jest --silent -u
35 | env:
36 | CI: true
37 | - name: Commit files to React Native Starter Kit repo
38 | run: |
39 | rm -rf .git
40 | git clone https://github.com/mcnamee/react-native-starter-kit.git TEMP
41 | mv TEMP/.git .git
42 | rm -rf TEMP
43 | git config --local user.email "${{ secrets.GH_EMAIL }}"
44 | git config --local user.name "mcnamee/react-native-boilerplate-builder"
45 | git add -A
46 | git commit -m "RNBB Bot updates" -a
47 | - name: Push files to React Native Starter Kit repo
48 | uses: ad-m/github-push-action@master
49 | with:
50 | github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
51 | repository: mcnamee/react-native-starter-kit
52 | branch: master
53 |
--------------------------------------------------------------------------------
/.github/workflows/web-build-test.yml:
--------------------------------------------------------------------------------
1 | name: Web Build + Test
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * MON'
6 | push:
7 | branches:
8 | - master
9 | pull_request:
10 | branches:
11 | - master
12 |
13 | jobs:
14 | build:
15 | runs-on: macOS-latest
16 | strategy:
17 | matrix:
18 | node-version: [14.x]
19 |
20 | steps:
21 | - uses: actions/checkout@v1
22 | - name: Use Node.js ${{ matrix.node-version }}
23 | uses: actions/setup-node@v1
24 | with:
25 | node-version: ${{ matrix.node-version }}
26 | - name: Get Commit Message
27 | run: |
28 | echo "::set-env name=COMMIT_MESSAGE::$(git log -1 --pretty=format:%s)"
29 | - name: Build a fresh React App
30 | run: |
31 | bash build.sh --name=ReactStarterKit --type=1
32 | - name: Lint
33 | run: |
34 | ./node_modules/.bin/eslint "src/**/*.js"
35 | - name: Jest Tests
36 | run: |
37 | yarn test -u
38 | env:
39 | CI: true
40 | - name: Commit files to React Starter Kit repo
41 | run: |
42 | rm -rf .git
43 | git clone https://github.com/mcnamee/react-starter-kit.git TEMP
44 | mv TEMP/.git .git
45 | rm -rf TEMP
46 | git config --local user.email "${{ secrets.GH_EMAIL }}"
47 | git config --local user.name "mcnamee/react-native-boilerplate-builder"
48 | git add -A
49 | git commit -m "$COMMIT_MESSAGE (RNBB Bot)" -a
50 | - name: Push files to React Starter Kit repo
51 | uses: ad-m/github-push-action@master
52 | with:
53 | github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
54 | repository: mcnamee/react-starter-kit
55 | branch: master
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # node.js
6 | #
7 | node_modules/
8 | npm-debug.log
9 | yarn-error.log
10 |
--------------------------------------------------------------------------------
/OURS-EXPO/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Expo Publish
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | publish:
8 | name: Install and publish
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - uses: actions/setup-node@v1
13 | with:
14 | node-version: 12.x
15 | - uses: expo/expo-github-action@v5
16 | with:
17 | expo-version: 3.x
18 | expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
19 | expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
20 | - run: yarn install
21 | - run: expo publish
22 |
--------------------------------------------------------------------------------
/OURS-EXPO/README.md:
--------------------------------------------------------------------------------
1 |
28 |
29 | ---
30 |
31 | ### Looking for something else?
32 |
33 | - [React Native Starter Kit (without Expo) / Boilerplate](https://github.com/mcnamee/react-native-starter-kit)
34 | - [React Starter Kit (web) / Boilerplate](https://github.com/mcnamee/react-starter-kit)
35 | - [Previous Version (React Starter Kit (Web + Native) w/ Firebase)](https://github.com/mcnamee/react-native-starter-kit/tree/archive/v3)
36 |
37 | ---
38 |
39 | ## 👋 Intro
40 |
41 | This project was bootstrapped with the [React Boilerplate Builder](https://github.com/mcnamee/react-native-boilerplate-builder) by [Matt McNamee](https://mcnam.ee).
42 |
43 | The project is _super_ helpful to kick-start your next project, as it provides a lot of the common tools you may reach for, all ready to go. Specifically:
44 |
45 | - __[Expo](https://expo.io/)__ - The fastest way to build an app
46 | - __Flux architecture__
47 | - [Redux](https://redux.js.org/docs/introduction/)
48 | - Redux Wrapper: [Rematch](https://github.com/rematch/rematch)
49 | - __Routing and navigation__
50 | - [React Native Router Flux](https://github.com/aksonov/react-native-router-flux)
51 | - __Data Caching / Offline__
52 | - [Redux Persist](https://github.com/rt2zz/redux-persist)
53 | - __UI Toolkit/s__
54 | - [Native Base](https://nativebase.io/)
55 | - __Code Linting__ with
56 | - [Airbnb's JS Linting](https://github.com/airbnb/javascript) guidelines
57 |
58 | ---
59 |
60 | ## 🚀 Getting Started
61 |
62 | - Install `eslint`, `prettier` and `editor config` plugins into your IDE
63 | - Ensure your machine has the Expo CLI Installed (`npm install -g expo-cli`)
64 |
65 | ```bash
66 | # Install dependencies
67 | yarn install
68 |
69 | # Start the App
70 | # - The Expo CLI will provide options to open in [web, android or iOS]
71 | yarn start
72 | ```
73 |
74 | ---
75 |
76 | ## 📖 Docs
77 |
78 | - [Contributing to this project](documentation/contributing.md)
79 | - [Tests & testing](documentation/testing.md)
80 | - [Understanding the file structure](documentation/file-structure.md)
81 |
82 | ---
83 |
84 | ## 👊 Further Help?
85 |
86 | This repo is a great place to start. But...if you'd prefer to sit back and have your new project built for you or just need some consultation, [get in touch with me directly](https://mcnam.ee) and I can organise a quote.
87 |
--------------------------------------------------------------------------------
/OURS-EXPO/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-EXPO/assets/icon.png
--------------------------------------------------------------------------------
/OURS-EXPO/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-EXPO/assets/splash.png
--------------------------------------------------------------------------------
/OURS-EXPO/documentation/expo-demo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-EXPO/documentation/expo-demo.jpg
--------------------------------------------------------------------------------
/OURS-EXPO/documentation/rnsk-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-EXPO/documentation/rnsk-logo.jpg
--------------------------------------------------------------------------------
/OURS-EXPO/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as Font from 'expo-font';
3 | import PropTypes from 'prop-types';
4 | import { Provider } from 'react-redux';
5 | import { Router, Stack } from 'react-native-router-flux';
6 | import { PersistGate } from 'redux-persist/es/integration/react';
7 |
8 | import { Root, StyleProvider } from 'native-base';
9 | import getTheme from '../native-base-theme/components';
10 | import theme from '../native-base-theme/variables/commonColor';
11 |
12 | import Routes from './routes/index';
13 | import Loading from './components/UI/Loading';
14 |
15 | class App extends React.Component {
16 | constructor() {
17 | super();
18 | this.state = { loading: true };
19 | }
20 |
21 | async componentDidMount() {
22 | await Font.loadAsync({
23 | Roboto: require('native-base/Fonts/Roboto.ttf'),
24 | Roboto_medium: require('native-base/Fonts/Roboto_medium.ttf'),
25 | Ionicons: require('@expo/vector-icons/build/vendor/react-native-vector-icons/Fonts/Ionicons.ttf'),
26 | });
27 |
28 | this.setState({ loading: false });
29 | }
30 |
31 | render() {
32 | const { loading } = this.state;
33 | const { store, persistor } = this.props;
34 |
35 | if (loading) {
36 | return ;
37 | }
38 |
39 | return (
40 |
41 |
42 | } persistor={persistor}>
43 |
44 |
45 | {Routes}
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | }
54 |
55 | App.propTypes = {
56 | store: PropTypes.shape({}).isRequired,
57 | persistor: PropTypes.shape({}).isRequired,
58 | };
59 |
60 | export default App;
61 |
--------------------------------------------------------------------------------
/OURS-NATIVE/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Root from './src/index';
3 | import configureStore from './src/store/index';
4 |
5 | const { persistor, store } = configureStore();
6 |
7 | export default function App() {
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/OURS-NATIVE/README.md:
--------------------------------------------------------------------------------
1 |
26 |
27 | ---
28 |
29 | ### Looking for something else?
30 |
31 | - [React Native Starter Kit (Expo) / Boilerplate](https://github.com/mcnamee/react-native-expo-starter-kit)
32 | - [React Starter Kit (web) / Boilerplate](https://github.com/mcnamee/react-starter-kit)
33 | - [Previous Version (React Starter Kit (Web + Native) w/ Firebase)](https://github.com/mcnamee/react-native-starter-kit/tree/archive/v3)
34 |
35 | ---
36 |
37 | ## 👋 Intro
38 |
39 | This project was bootstrapped with the [React Boilerplate Builder](https://github.com/mcnamee/react-native-boilerplate-builder) by [Matt McNamee](https://mcnam.ee).
40 |
41 | The project is _super_ helpful to kick-start your next project, as it provides a lot of the common tools you may reach for, all ready to go. Specifically:
42 |
43 | - __Flux architecture__
44 | - [Redux](https://redux.js.org/docs/introduction/)
45 | - Redux Wrapper: [Rematch](https://github.com/rematch/rematch)
46 | - __Routing and navigation__
47 | - [React Native Router Flux](https://github.com/aksonov/react-native-router-flux) for native mobile navigation
48 | - __Data Caching / Offline__
49 | - [Redux Persist](https://github.com/rt2zz/redux-persist)
50 | - __UI Toolkit/s__
51 | - [Native Base](https://nativebase.io/) for native mobile
52 | - __Code Linting__ with
53 | - [Airbnb's JS Linting](https://github.com/airbnb/javascript) guidelines
54 | - __Deployment strategy__
55 | - [Both manual and automated strategies](documentation/deploy.md)
56 | - __Splash Screen + Assets__
57 | - [React Native Splash Screen](https://github.com/crazycodeboy/react-native-splash-screen)
58 |
59 | ---
60 |
61 | ## 🚀 Getting Started
62 |
63 | - Install [React Native Debugger](https://github.com/jhen0409/react-native-debugger/releases) and open before running the app
64 | - Install `eslint`, `prettier` and `editor config` plugins into your IDE
65 | - Ensure your machine has the [React Native dependencies installed](https://facebook.github.io/react-native/docs/getting-started)
66 |
67 | ```bash
68 | # Install dependencies
69 | yarn install && ( cd ios && pod install )
70 | ```
71 |
72 | #### iOS
73 |
74 | ```bash
75 | # Start in the iOS Simulator
76 | npx react-native run-ios --simulator="iPhone 11"
77 | ```
78 |
79 | #### Android
80 |
81 | ```bash
82 | # Start in the Android Simulator
83 | # - Note: open Android Studio > Tools > AVD > Run a device
84 | # - Example device specs: https://medium.com/pvtl/react-native-android-development-on-mac-ef7481f65e47#d5da
85 | npx react-native run-android
86 | ```
87 |
88 | ---
89 |
90 | ## 📖 Docs
91 |
92 | - [Contributing to this project](documentation/contributing.md)
93 | - [FAQs & Opinions](documentation/faqs.md)
94 | - [Tests & testing](documentation/testing.md)
95 | - [Understanding the file structure](documentation/file-structure.md)
96 | - [Deploy the app](documentation/deploy.md)
97 |
98 | ---
99 |
100 | ## 👊 Further Help?
101 |
102 | This repo is a great place to start. But...if you'd prefer to sit back and have your new project built for you or just need some consultation, [get in touch with me directly](https://mcnam.ee) and I can organise a quote.
103 |
--------------------------------------------------------------------------------
/OURS-NATIVE/android/app/src/main/res/drawable-xxhdpi/launch_screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-NATIVE/android/app/src/main/res/drawable-xxhdpi/launch_screen.png
--------------------------------------------------------------------------------
/OURS-NATIVE/android/app/src/main/res/layout/launch_screen.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/OURS-NATIVE/documentation/deploy.md:
--------------------------------------------------------------------------------
1 | # 🚀 Deploying
2 |
3 | ## Setting up a new app:
4 |
5 | The following steps should be followed for new projects. Once completed for your project, you won't need these steps again.
6 |
7 | *General*
8 |
9 | 1. Ensure you have admin access to the destination Google Play and Apple/iTunesConnect Developer accounts
10 | 1. Ensure you've named your app correctly and set a unique bundle identifier:
11 | - Use [react-native-rename](https://www.npmjs.com/package/react-native-rename)
12 | - eg. `react-native-rename "Travel App" -b com.junedomingo.travelapp`
13 | - Open the project in Xcode and double check that the Bundle ID has been updated (if not, correct it)
14 | 1. In both Google Play and iTunes Connect:
15 | - Setup a new app
16 | - Use the _manual_ method below to build and deploy the app for the first time
17 | - _iOS Note: when you deploy the iOS app for the first time, you'll select 'Automatic Key Management'. Xcode will generate a private distribution key. Ensure you save this (eg. to a password safe) so that others can distribute the app too_
18 |
19 | *Android*
20 |
21 | 1. Generate/configure Android key:
22 | - `( cd android/app && keytool -genkeypair -v -keystore android-release-key.keystore -alias jims-app-release-key -keyalg RSA -keysize 2048 -validity 10000 )` (note: change `jims-app-release-key` to your own alias)
23 | - Save the key to a secure password safe (don't commit it to the repo)
24 | 1. [Setup the Gradle variables](https://reactnative.dev/docs/signed-apk-android#setting-up-gradle-variables), using the alias and password/s (that you set in the previous command) in: `android/gradle.properties`
25 | 1. [Add the release signing config to your app's Gradle config](https://reactnative.dev/docs/signed-apk-android#adding-signing-config-to-your-apps-gradle-config) in: `android/app/build.gradle`
26 |
27 | *Fastlane*
28 |
29 | 1. Using the __account owner's__ login (i.e. we want to create the API credentials from the owner's account) - follow the [steps here](https://docs.fastlane.tools/actions/supply/#setup) to generate API credentials for Google Play. Download and place the json file here: `android/app/google-play-android-developer.json`. Save the key to a secure password safe (don't commit it to the repo)
30 | 1. Update the `package_name` and `itc_team_id` (App Store Connect Team ID) in `faslane/Appfile` to match the bundle of your app
31 | 1. Update the following in `fastlane/Fastfile`:
32 | - `app_identifier: com.app.bundle` - where com.app.bundle is your bundle id
33 | - `name.xcodeproj` - to the name of your Xcode project file
34 | - `scheme: 'name'` - where name is your scheme (eg. AppName)
35 | 1. Run `fastlane supply init` (which will download the meta data of the uploaded app, from the stores)
36 |
37 | ---
38 |
39 | ## Configuring your machine to deploy:
40 |
41 | The following steps are provided for developers who have the project setup on their machine, but have not yet deployed the app. Follow these once, and you won't need these steps again.
42 |
43 | 1. Android (Google Play):
44 | - Add the Android keys (found in the password safe) to your local project:
45 | - `android/app/android-release-key.keystore`
46 | - `android/app/google-play-android-developer.json`
47 | - [Android/Google dependencies](https://facebook.github.io/react-native/docs/getting-started#installing-dependencies-1)
48 | 1. iOS (Apple iTunes Connect):
49 | - In Xcode, login to the appropriate account to give you access to deploy
50 | - Install the appropriate distribution private key (found in your password safe)
51 | - Download the file and double click it to add to Keychain
52 | 1. Fastlane (for automated deployments on both platforms):
53 | - Install Fastlane - `brew cask install fastlane`
54 | - Install Xcode command line tools - `xcode-select --install`
55 |
56 | ---
57 |
58 | ## Deploying
59 |
60 | - Update the __app version__ - `bash fastlane/update-app-version.sh`
61 | - __Merge__ `develop` branch into `master` branch with a _merge commit_
62 | - Git __Tag__ the master merge commit. The tag name should be the new version number
63 | - Bundle and deploy by the following:
64 |
65 | ### 1.0 (Automated) Fastlane
66 |
67 | Fastlane automatically builds and deploys the app to the app stores (TestFlight and Play Store Beta).
68 |
69 | 1. _Hint: Did you update the version number, merge to master and tag?_
70 | 1. __iOS__: Deploy to Apple TestFlight - `fastlane ios beta`
71 | 1. __Android__: Deploy to Google Play Beta - `fastlane android beta`
72 |
73 | ### 2.0 Manual
74 |
75 | *2.2.1 iOS*
76 |
77 | _*Note: it may be required to use the legacy build system (XCode -> File -> Project Settings -> Change the build system to 'Legacy Build System')_
78 |
79 | 1. _Hint: Did you update the version number, merge to master and tag?_
80 | 1. Ensure you've changed the Xcode 'Build Config' to Release
81 | 1. Select 'Generic iOS Device' from devices
82 | 1. Product > Archive
83 | 1. Open Organiser
84 | - Find the archive and click 'Validate' to check that it's ok
85 | - Click the big 'Upload to App Store...' when ready (untick BitCode checkbox)
86 |
87 | *2.2.2 Android*
88 |
89 | 1. _Hint: Did you update the version number, merge to master and tag?_
90 | 1. `( cd android && ./gradlew app:bundleRelease )`
91 | 1. Upload the generated file (`/android/app/build/outputs/bundle/release/app.aab`) to Google Play
92 |
--------------------------------------------------------------------------------
/OURS-NATIVE/documentation/faqs.md:
--------------------------------------------------------------------------------
1 | # FAQs
2 |
3 | ## Code Style Guide?
4 |
5 | We're using [Airbnb's](https://github.com/airbnb/javascript) JS/React Style Guide with ESLint linting. We just like it :)
6 |
7 | ## React, hah? How do I?
8 |
9 | [React Native Express](http://www.reactnativeexpress.com/) is a great site to get you started, specifically:
10 |
11 | - [Get your head around ES6](http://www.reactnativeexpress.com/es6)
12 | - [What is JSX?](http://www.reactnativeexpress.com/jsx)
13 | - [What are Components?](http://www.reactnativeexpress.com/components)
14 | - [React State](http://www.reactnativeexpress.com/data_component_state)
15 | - [Redux](http://www.reactnativeexpress.com/redux)
16 | - [Rematch](https://rematch.gitbooks.io/rematch/)
17 |
18 | Once you've got your head around the basics, checkout the [React Native](https://facebook.github.io/react-native/) and [React](https://reactjs.org/) websites, specifically
19 |
20 | - Go through ['The Basics'](https://facebook.github.io/react-native/docs/props.html)
21 | - Gain an understanding of the [components](https://facebook.github.io/react-native/docs/activityindicator.html) React Native provides out of the box
22 |
23 | ## How do I change the Reach Native App Icon?
24 |
25 | You might want to change the app icons for iOS and Android. You can use the [app-icon](https://github.com/dwmkerr/app-icon) utility to generate all of the required icons for each required size.
26 |
27 | ```bash
28 | npx app-icon generate -i ./src/images/app-icon.png
29 | ```
30 |
31 | This will generate the icon in all required sizes. You can also add labels to icons, which can be useful for testing. This example labels the icon with 'beta' and the current version number:
32 |
33 | ```bash
34 | npx app-icon label -i ./src/images/app-icon.png -o temp.png --top beta --bottom $(jq .version package.json)
35 | npx app-icon generate -i temp.png
36 | ```
37 |
38 | 
39 |
40 | ## How do I change the React Native App Name/Bundle ID?
41 |
42 | - Use [react-native-rename](https://www.npmjs.com/package/react-native-rename)
43 | - eg. `npx react-native-rename "The Facebook" -b com.thefacebook.mobile-app`
44 | - Open the project in Xcode and double check that the Bundle ID has been updated (if not, correct it)
45 |
--------------------------------------------------------------------------------
/OURS-NATIVE/documentation/file-structure.md:
--------------------------------------------------------------------------------
1 | ## File structure
2 |
3 | - `/android` - contains native code specific to the Android OS
4 | - `/documentation` - as the name suggests - any docs
5 | - `/fastlane` - configuration for auto-deploying the app to the app stores via Fastlane
6 | - `/ios` - native code specific to iOS
7 | - `/native-base-theme` - the app uses Native Base for base elements. You can edit the styles in here
8 | - `/src` - contains our JS and CSS code. `index.js` is the entry-point for our file, and is mandatory.
9 | - `/components` - 'Dumb-components' / presentational. [Read More →](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
10 | - `/constants` - App-wide variables
11 | - `/containers` - 'Smart-components' that connect business logic to presentation [Read More →](https://redux.js.org/docs/basics/UsageWithReact.html#presentational-and-container-components)
12 | - `/images` - hmm...what could this be?
13 | - `/lib` - Utils and custom libraries
14 | - `/models` - Rematch models combining actions, reducers and state. [Read More →](https://github.com/rematch/rematch#step-2-models)
15 | - `/routes`- wire up the router with any & all screens [Read More →](https://github.com/aksonov/react-native-router-flux)
16 | - `/store`- Redux Store - hooks up the stores and provides initial/template states [Read More →](https://redux.js.org/docs/basics/Store.html)
17 | - `/tests` - contains all of our tests, where the test file matches the resptive file from `/src`
18 | - `index.js` - The starting place for our app
19 |
--------------------------------------------------------------------------------
/OURS-NATIVE/documentation/rnsk-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-NATIVE/documentation/rnsk-logo.jpg
--------------------------------------------------------------------------------
/OURS-NATIVE/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | json_key_file("android/app/google-play-android-developer.json") # Path to the Android json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
2 | package_name("com.AwesomeProject") # e.g. com.krausefx.app
3 | # itc_team_id "" # e.g. 1233445 - the iTunes Connect Team ID
4 |
--------------------------------------------------------------------------------
/OURS-NATIVE/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | fastlane_version '2.125.0'
2 |
3 | before_all do
4 | # ensure_git_branch
5 | # ensure_git_status_clean
6 | # git_pull
7 | end
8 |
9 | platform :ios do
10 | desc 'Fetch certificates and provisioning profiles'
11 | lane :certificates do
12 | match(app_identifier: 'com.AwesomeProject.app', type: 'development', readonly: true)
13 | match(app_identifier: 'com.AwesomeProject.app', type: 'appstore', readonly: true)
14 | end
15 |
16 | desc 'Build the iOS application.'
17 | private_lane :build do
18 | # certificates
19 | # increment_build_number(xcodeproj: './ios/AwesomeProject.xcodeproj')
20 | gym(scheme: 'AwesomeProject', workspace: './ios/AwesomeProject.xcworkspace')
21 | end
22 |
23 | desc 'Ship to Testflight.'
24 | lane :beta do
25 | build
26 | pilot
27 | # commit_version_bump(message: 'Bump build', xcodeproj: './ios/AwesomeProject.xcodeproj')
28 | # push_to_git_remote
29 | end
30 | end
31 |
32 | platform :android do
33 | desc 'Build the Android application.'
34 | private_lane :build do
35 | gradle(task: 'clean', project_dir: 'android/')
36 | gradle(task: 'bundle', build_type: 'Release', project_dir: 'android/')
37 | end
38 |
39 | desc 'Ship to Playstore Beta.'
40 | lane :beta do
41 | build
42 | supply(track: 'beta', track_promote_to: 'beta')
43 | # git_commit(path: ['./android/gradle.properties'], message: 'Bump versionCode')
44 | # push_to_git_remote
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/OURS-NATIVE/fastlane/README.md:
--------------------------------------------------------------------------------
1 | fastlane documentation
2 | ================
3 | # Installation
4 |
5 | Make sure you have the latest version of the Xcode command line tools installed:
6 |
7 | ```
8 | xcode-select --install
9 | ```
10 |
11 | Install _fastlane_ using
12 | ```
13 | [sudo] gem install fastlane -NV
14 | ```
15 | or alternatively using `brew cask install fastlane`
16 |
17 | # Available Actions
18 | ## iOS
19 | ### ios certificates
20 | ```
21 | fastlane ios certificates
22 | ```
23 | Fetch certificates and provisioning profiles
24 | ### ios beta
25 | ```
26 | fastlane ios beta
27 | ```
28 | Ship to Testflight.
29 |
30 | ----
31 |
32 | ## Android
33 | ### android beta
34 | ```
35 | fastlane android beta
36 | ```
37 | Ship to Playstore Beta.
38 |
39 | ----
40 |
41 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
42 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
43 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
44 |
--------------------------------------------------------------------------------
/OURS-NATIVE/fastlane/metadata/android/en-GB/full_description.txt:
--------------------------------------------------------------------------------
1 | AwesomeProject
2 |
--------------------------------------------------------------------------------
/OURS-NATIVE/fastlane/metadata/android/en-GB/short_description.txt:
--------------------------------------------------------------------------------
1 | AwesomeProject
2 |
--------------------------------------------------------------------------------
/OURS-NATIVE/fastlane/metadata/android/en-GB/title.txt:
--------------------------------------------------------------------------------
1 | AwesomeProject
2 |
--------------------------------------------------------------------------------
/OURS-NATIVE/fastlane/metadata/android/en-GB/video.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-NATIVE/fastlane/metadata/android/en-GB/video.txt
--------------------------------------------------------------------------------
/OURS-NATIVE/fastlane/update-app-version.sh:
--------------------------------------------------------------------------------
1 | ANDROID_FILE="android/app/build.gradle"
2 | TEMP_ANDROID_FILE="${ANDROID_FILE}.txt"
3 |
4 | IOS_FILE="ios/AwesomeProject/Info.plist"
5 | TEMP_IOS_FILE="${IOS_FILE}.txt"
6 |
7 | IOS_BUILD_NUMBER=1
8 |
9 | CURRENT_VERSION_NAME=$( /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "${IOS_FILE}" )
10 |
11 | # ---
12 |
13 | echo "••• What's the new App Version? (current version: $CURRENT_VERSION_NAME)"
14 | read APP_VERSION_NAME
15 |
16 | # ---
17 |
18 | # Android
19 | cat ${ANDROID_FILE} | sed "s/versionName \".*\"/versionName \"${APP_VERSION_NAME}\"/" > ${TEMP_ANDROID_FILE}
20 | echo "$(awk '{sub(/versionCode [[:digit:]]+$/,"versionCode "$2+1)}1' ${TEMP_ANDROID_FILE})" > ${TEMP_ANDROID_FILE}
21 | cat ${TEMP_ANDROID_FILE} > ${ANDROID_FILE}
22 | rm -f ${TEMP_ANDROID_FILE}
23 |
24 | # iOS
25 | /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${APP_VERSION_NAME}" "${IOS_FILE}"
26 | /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${IOS_BUILD_NUMBER}" "${IOS_FILE}"
27 |
--------------------------------------------------------------------------------
/OURS-NATIVE/index.js:
--------------------------------------------------------------------------------
1 | import { AppRegistry } from 'react-native';
2 | import App from './App';
3 | import { name as appName } from './app.json';
4 |
5 | AppRegistry.registerComponent(appName, () => App);
6 |
--------------------------------------------------------------------------------
/OURS-NATIVE/ios/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/OURS-NATIVE/ios/Images.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-NATIVE/ios/Images.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/OURS-NATIVE/ios/Images.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-NATIVE/ios/Images.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/OURS-NATIVE/ios/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "120.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "180.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "idiom" : "ios-marketing",
47 | "size" : "1024x1024",
48 | "scale" : "1x"
49 | }
50 | ],
51 | "info" : {
52 | "version" : 1,
53 | "author" : "xcode"
54 | }
55 | }
--------------------------------------------------------------------------------
/OURS-NATIVE/ios/Images.xcassets/LaunchScreen.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "original"
25 | }
26 | }
--------------------------------------------------------------------------------
/OURS-NATIVE/ios/Images.xcassets/LaunchScreen.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-NATIVE/ios/Images.xcassets/LaunchScreen.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/OURS-NATIVE/ios/Images.xcassets/LaunchScreen.imageset/LaunchImage2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-NATIVE/ios/Images.xcassets/LaunchScreen.imageset/LaunchImage2x.png
--------------------------------------------------------------------------------
/OURS-NATIVE/ios/Images.xcassets/LaunchScreen.imageset/LaunchImage3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-NATIVE/ios/Images.xcassets/LaunchScreen.imageset/LaunchImage3x.png
--------------------------------------------------------------------------------
/OURS-NATIVE/src/components/About.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Container, Content, Text, H1, H2, H3,
4 | } from 'native-base';
5 | import Spacer from './UI/Spacer';
6 |
7 | const About = () => (
8 |
9 |
10 |
11 | Heading 1
12 |
13 |
14 | Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus
15 | commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.
16 | Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.
17 | {' '}
18 |
19 |
20 |
21 | Heading 2
22 |
23 |
24 | Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus
25 | commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.
26 | Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.
27 | {' '}
28 |
29 |
30 |
31 | Heading 3
32 |
33 |
34 | Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus
35 | commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.
36 | Etiam porta sem malesuada magna mollis euismod. Donec sed odio dui.
37 | {' '}
38 |
39 |
40 |
41 | );
42 |
43 | export default About;
44 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/components/Articles/Form.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useForm } from 'react-hook-form';
4 | import {
5 | Container,
6 | Content,
7 | Text,
8 | Form,
9 | Item,
10 | Label,
11 | Input,
12 | Button,
13 | } from 'native-base';
14 | import { Messages, Header, Spacer } from '../UI';
15 | import { errorMessages } from '../../constants/messages';
16 |
17 | const ArticlesForm = ({
18 | error, loading, success, onFormSubmit, defaultValues,
19 | }) => {
20 | const {
21 | register, handleSubmit, errors, setValue,
22 | } = useForm({ defaultValues });
23 |
24 | useEffect(() => {
25 | register({ name: 'email' }, { required: errorMessages.missingEmail });
26 | }, [register]);
27 |
28 | return (
29 |
30 |
31 |
35 |
36 | {error && }
37 | {loading && }
38 | {success && }
39 |
40 |
60 |
61 |
62 | );
63 | };
64 |
65 | ArticlesForm.propTypes = {
66 | error: PropTypes.string,
67 | loading: PropTypes.bool,
68 | success: PropTypes.string,
69 | defaultValues: PropTypes.shape({
70 | email: PropTypes.string,
71 | }),
72 | onFormSubmit: PropTypes.func.isRequired,
73 | };
74 |
75 | ArticlesForm.defaultProps = {
76 | error: null,
77 | success: null,
78 | loading: false,
79 | defaultValues: {},
80 | };
81 |
82 | export default ArticlesForm;
83 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/components/Articles/List.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Actions } from 'react-native-router-flux';
4 | import { FlatList, TouchableOpacity, Image } from 'react-native';
5 | import {
6 | Container, Card, CardItem, Body, Text, Button,
7 | } from 'native-base';
8 | import { Error, Spacer } from '../UI';
9 | import { errorMessages } from '../../constants/messages';
10 |
11 | const ArticlesList = ({
12 | error, loading, listFlat, reFetch, meta,
13 | }) => {
14 | if (error) {
15 | return ;
16 | }
17 |
18 | if (listFlat.length < 1) {
19 | return ;
20 | }
21 |
22 | return (
23 |
24 | reFetch({ forceSync: true })}
27 | refreshing={loading}
28 | renderItem={({ item }) => (
29 |
30 | (
33 | !item.placeholder
34 | ? Actions.articlesSingle({ id: item.id, title: item.name })
35 | : null
36 | )}
37 | style={{ flex: 1 }}
38 | >
39 |
40 | {!!item.image && (
41 |
53 | )}
54 |
55 |
56 |
57 |
58 | {item.name}
59 |
60 | {!!item.excerpt && {item.excerpt} }
61 |
62 |
63 |
64 |
65 |
66 | )}
67 | keyExtractor={(item) => `${item.id}-${item.name}`}
68 | ListFooterComponent={(meta && meta.page && meta.lastPage && meta.page < meta.lastPage)
69 | ? () => (
70 |
71 |
72 | reFetch({ incrementPage: true })}
76 | >
77 | Load More
78 |
79 |
80 | ) : null}
81 | />
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | ArticlesList.propTypes = {
89 | error: PropTypes.string,
90 | loading: PropTypes.bool,
91 | listFlat: PropTypes.arrayOf(
92 | PropTypes.shape({
93 | placeholder: PropTypes.bool,
94 | id: PropTypes.number,
95 | name: PropTypes.string,
96 | date: PropTypes.string,
97 | content: PropTypes.string,
98 | excerpt: PropTypes.string,
99 | image: PropTypes.string,
100 | }),
101 | ),
102 | reFetch: PropTypes.func,
103 | meta: PropTypes.shape({ page: PropTypes.number, lastPage: PropTypes.number }),
104 | };
105 |
106 | ArticlesList.defaultProps = {
107 | listFlat: [],
108 | error: null,
109 | reFetch: null,
110 | meta: { page: null, lastPage: null },
111 | loading: false,
112 | };
113 |
114 | export default ArticlesList;
115 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/components/Articles/Single.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Image } from 'react-native';
4 | import {
5 | Container, Content, Card, CardItem, Body, H3, Text,
6 | } from 'native-base';
7 | import { Loading, Error, Spacer } from '../UI';
8 | import { errorMessages } from '../../constants/messages';
9 |
10 | const ArticlesSingle = ({
11 | error, loading, article, reFetch,
12 | }) => {
13 | if (error) {
14 | return ;
15 | }
16 |
17 | if (loading) {
18 | return ;
19 | }
20 |
21 | if (Object.keys(article).length < 1) {
22 | return ;
23 | }
24 |
25 | return (
26 |
27 |
28 | {!!article.image && (
29 |
35 | )}
36 |
37 |
38 | {article.name}
39 |
40 |
41 | {!!article.content && (
42 |
43 |
44 | Content
45 |
46 |
47 |
48 | {article.content}
49 |
50 |
51 |
52 | )}
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | ArticlesSingle.propTypes = {
60 | error: PropTypes.string,
61 | loading: PropTypes.bool,
62 | article: PropTypes.shape(),
63 | reFetch: PropTypes.func,
64 | };
65 |
66 | ArticlesSingle.defaultProps = {
67 | error: null,
68 | loading: false,
69 | article: {},
70 | reFetch: null,
71 | };
72 |
73 | export default ArticlesSingle;
74 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/components/UI/Error.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | Container, Text, H3, Button, View,
5 | } from 'native-base';
6 | import Spacer from './Spacer';
7 |
8 | const Error = ({ title, content, tryAgain }) => (
9 |
10 |
11 |
12 | {title}
13 | {content}
14 | {tryAgain && (
15 |
16 | Try Again
17 |
18 | )}
19 |
20 |
21 |
22 | );
23 |
24 | Error.propTypes = {
25 | title: PropTypes.string,
26 | content: PropTypes.string,
27 | tryAgain: PropTypes.func,
28 | };
29 |
30 | Error.defaultProps = {
31 | title: 'Uh oh',
32 | content: 'An unexpected error came up',
33 | tryAgain: null,
34 | };
35 |
36 | export default Error;
37 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/components/UI/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { View } from 'react-native';
4 | import { Text, H1 } from 'native-base';
5 | import Spacer from './Spacer';
6 |
7 | const Header = ({ title, content }) => (
8 |
9 |
10 | {title}
11 | {!!content && (
12 |
13 |
14 | {content}
15 |
16 | )}
17 |
18 |
19 | );
20 |
21 | Header.propTypes = {
22 | title: PropTypes.string,
23 | content: PropTypes.string,
24 | };
25 |
26 | Header.defaultProps = {
27 | title: 'Missing title',
28 | content: '',
29 | };
30 |
31 | export default Header;
32 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/components/UI/Loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, ActivityIndicator } from 'react-native';
3 | import Colors from '../../../native-base-theme/variables/commonColor';
4 |
5 | const Loading = () => (
6 |
7 |
8 |
9 | );
10 |
11 | export default Loading;
12 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/components/UI/Messages.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { View } from 'react-native';
4 | import { Text } from 'native-base';
5 |
6 | import Colors from '../../../native-base-theme/variables/commonColor';
7 |
8 | const Messages = ({ message, type }) => (
9 |
17 | {message}
18 |
19 | );
20 |
21 | Messages.propTypes = {
22 | message: PropTypes.string,
23 | type: PropTypes.oneOf(['error', 'success', 'info']),
24 | };
25 |
26 | Messages.defaultProps = {
27 | message: 'An unexpected error came up',
28 | type: 'error',
29 | };
30 |
31 | export default Messages;
32 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/components/UI/Spacer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { View } from 'native-base';
4 |
5 | const Spacer = ({ size }) => ;
6 |
7 | Spacer.propTypes = {
8 | size: PropTypes.number,
9 | };
10 |
11 | Spacer.defaultProps = {
12 | size: 20,
13 | };
14 |
15 | export default Spacer;
16 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/components/UI/index.js:
--------------------------------------------------------------------------------
1 | export { default as Error } from './Error';
2 | export { default as Header } from './Header';
3 | export { default as Loading } from './Loading';
4 | export { default as Messages } from './Messages';
5 | export { default as Spacer } from './Spacer';
6 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/constants/config.js:
--------------------------------------------------------------------------------
1 | const isDevEnv = process.env.NODE_ENV === 'development';
2 |
3 | export default {
4 | // App Details
5 | appName: 'AwesomeProject',
6 |
7 | // Build Configuration - eg. Debug or Release?
8 | isDevEnv,
9 |
10 | // Date Format
11 | dateFormat: 'Do MMM YYYY',
12 |
13 | // API
14 | apiBaseUrl: isDevEnv
15 | ? 'https://digitalsupply.co/wp-json/wp'
16 | : 'https://digitalsupply.co/wp-json/wp',
17 |
18 | // Google Analytics - uses a 'dev' account while we're testing
19 | gaTrackingId: isDevEnv ? 'UA-84284256-2' : 'UA-84284256-1',
20 | };
21 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/constants/navigation.js:
--------------------------------------------------------------------------------
1 | import Colors from '../../native-base-theme/variables/commonColor';
2 |
3 | export default {
4 | navbarProps: {
5 | navigationBarStyle: { backgroundColor: 'white' },
6 | titleStyle: {
7 | color: Colors.textColor,
8 | alignSelf: 'center',
9 | fontSize: Colors.fontSizeBase,
10 | },
11 | backButtonTintColor: Colors.textColor,
12 | },
13 |
14 | tabProps: {
15 | swipeEnabled: false,
16 | activeBackgroundColor: 'rgba(255,255,255,0.1)',
17 | inactiveBackgroundColor: Colors.brandPrimary,
18 | tabBarStyle: { backgroundColor: Colors.brandPrimary },
19 | },
20 |
21 | icons: {
22 | style: { color: 'white', height: 30, width: 30 },
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Provider } from 'react-redux';
4 | import { Router, Stack } from 'react-native-router-flux';
5 | import { PersistGate } from 'redux-persist/es/integration/react';
6 | import SplashScreen from 'react-native-splash-screen';
7 |
8 | import { Root, StyleProvider } from 'native-base';
9 | import getTheme from '../native-base-theme/components';
10 | import theme from '../native-base-theme/variables/commonColor';
11 |
12 | import Routes from './routes/index';
13 | import Loading from './components/UI/Loading';
14 |
15 | class App extends React.Component {
16 | constructor() {
17 | super();
18 | this.state = { loading: true };
19 | }
20 |
21 | async componentDidMount() {
22 | SplashScreen.hide();
23 | this.setState({ loading: false });
24 | }
25 |
26 | render() {
27 | const { loading } = this.state;
28 | const { store, persistor } = this.props;
29 |
30 | if (loading) {
31 | return ;
32 | }
33 |
34 | return (
35 |
36 |
37 | } persistor={persistor}>
38 |
39 |
40 | {Routes}
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | App.propTypes = {
51 | store: PropTypes.shape({}).isRequired,
52 | persistor: PropTypes.shape({}).isRequired,
53 | };
54 |
55 | export default App;
56 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/lib/api.js:
--------------------------------------------------------------------------------
1 | import AsyncStorage from '@react-native-community/async-storage';
2 | import axios from 'axios';
3 | import Config from '../constants/config';
4 |
5 | /**
6 | * Axios defaults
7 | */
8 | axios.defaults.baseURL = Config.apiBaseUrl;
9 |
10 | // Headers
11 | axios.defaults.headers.common['Content-Type'] = 'application/json';
12 | axios.defaults.headers.common.Accept = 'application/json';
13 |
14 | /**
15 | * Request Interceptor
16 | */
17 | axios.interceptors.request.use(
18 | async (inputConfig) => {
19 | const config = inputConfig;
20 |
21 | // Check for and add the stored Auth Token to the header request
22 | let token = '';
23 | try {
24 | token = await AsyncStorage.getItem('@Auth:token');
25 | } catch (error) {
26 | /* Nothing */
27 | }
28 | if (token) {
29 | config.headers.common.Authorization = `Bearer ${token}`;
30 | }
31 |
32 | return config;
33 | },
34 | (error) => {
35 | throw error;
36 | },
37 | );
38 |
39 | /**
40 | * Response Interceptor
41 | */
42 | axios.interceptors.response.use(
43 | (res) => {
44 | // Status code isn't a success code - throw error
45 | if (!`${res.status}`.startsWith('2')) {
46 | throw res.data;
47 | }
48 |
49 | // Otherwise just return the data
50 | return res;
51 | },
52 | (error) => {
53 | // Pass the response from the API, rather than a status code
54 | if (error && error.response && error.response.data) {
55 | throw error.response.data;
56 | }
57 | throw error;
58 | },
59 | );
60 |
61 | export default axios;
62 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Scene, Tabs, Stack } from 'react-native-router-flux';
3 | import { Icon } from 'native-base';
4 | import DefaultProps from '../constants/navigation';
5 | import AppConfig from '../constants/config';
6 |
7 | import { ArticlesForm, ArticlesList, ArticlesSingle } from '../containers';
8 |
9 | import AboutComponent from '../components/About';
10 |
11 | const Index = (
12 |
13 |
14 |
21 | }
25 | {...DefaultProps.navbarProps}
26 | >
27 |
28 |
29 |
30 | }
34 | {...DefaultProps.navbarProps}
35 | >
36 |
37 |
38 |
39 |
40 | }
44 | {...DefaultProps.navbarProps}
45 | >
46 |
47 |
48 |
49 |
50 |
51 | );
52 |
53 | export default Index;
54 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/store/index.js:
--------------------------------------------------------------------------------
1 | /* global */
2 | import { init } from '@rematch/core';
3 | import createPersistPlugin, { getPersistor } from '@rematch/persist';
4 | import createLoadingPlugin from '@rematch/loading';
5 | import AsyncStorage from '@react-native-community/async-storage';
6 | import * as models from '../models';
7 |
8 | // Create plugins
9 | const persistPlugin = createPersistPlugin({
10 | key: 'root',
11 | storage: AsyncStorage,
12 | blacklist: [],
13 | });
14 | const loadingPlugin = createLoadingPlugin({});
15 |
16 | const configureStore = () => {
17 | const store = init({
18 | models,
19 | redux: {
20 | middlewares: [],
21 | },
22 | plugins: [persistPlugin, loadingPlugin],
23 | });
24 |
25 | const persistor = getPersistor();
26 | const { dispatch } = store;
27 |
28 | return { persistor, store, dispatch };
29 | };
30 |
31 | export default configureStore;
32 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/__mocks__/@react-native-community/async-storage.js:
--------------------------------------------------------------------------------
1 | export default from '@react-native-community/async-storage/jest/async-storage-mock';
2 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/components/Articles/List.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { render } from '@testing-library/react-native';
4 | import ArticlesList from '../../../components/Articles/List';
5 | import { errorMessages } from '../../../constants/messages';
6 |
7 | it(' shows a nice error message', () => {
8 | const Component = ;
9 |
10 | // Matches snapshot
11 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
12 |
13 | // Has the correct text on the page
14 | const { getByText } = render(Component);
15 | expect(getByText(errorMessages.articlesEmpty));
16 | });
17 |
18 | it(' shows a list of articles correctly', () => {
19 | const listItem = {
20 | id: 0,
21 | name: 'ABC',
22 | excerpt: 'DEF',
23 | contentRaw: 'DEF',
24 | date: '22/33/44',
25 | };
26 |
27 | const Component = ;
28 |
29 | // Matches snapshot
30 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
31 |
32 | // Has the correct text on the page
33 | const { getByText } = render(Component);
34 | expect(getByText(listItem.name));
35 | });
36 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/components/Articles/Single.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { render } from '@testing-library/react-native';
4 | import ArticlesSingle from '../../../components/Articles/Single';
5 | import { errorMessages } from '../../../constants/messages';
6 |
7 | it(' shows a nice error message', () => {
8 | const Component = ;
9 |
10 | // Matches snapshot
11 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
12 |
13 | // Has the correct text on the page
14 | const { getByText } = render(Component);
15 | expect(getByText(errorMessages.articles404));
16 | });
17 |
18 | it(' shows an article correctly', () => {
19 | const article = {
20 | id: 0,
21 | name: 'ABC',
22 | excerpt: 'DEF',
23 | content: 'DEF',
24 | date: '22/33/44',
25 | };
26 |
27 | const Component = ;
28 |
29 | // Matches snapshot
30 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
31 |
32 | // Has the correct text on the page
33 | const { getByText } = render(Component);
34 | expect(getByText(article.name));
35 | });
36 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/components/Articles/__snapshots__/Single.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` shows a nice error message 1`] = `
4 |
18 |
28 |
39 |
53 | Uh oh
54 |
55 |
71 | This article could not be found
72 |
73 |
84 |
85 |
86 | `;
87 |
88 | exports[` shows an article correctly 1`] = `
89 |
98 |
107 |
158 |
159 |
170 |
179 | ABC
180 |
181 |
192 |
218 |
236 |
247 | Content
248 |
249 |
250 |
264 |
273 |
283 | DEF
284 |
285 |
286 |
287 |
288 |
299 |
300 |
301 |
302 |
303 | `;
304 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/components/UI/Error.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { render } from '@testing-library/react-native';
4 | import Error from '../../../components/UI/Error';
5 |
6 | it(' renders with message', () => {
7 | const Component = ;
8 |
9 | // Matches snapshot
10 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
11 |
12 | // Has the correct text on the page
13 | const { getByText } = render(Component);
14 | expect(getByText('hello boy'));
15 | });
16 |
17 | it(' renders with a button', () => {
18 | const Component = {}} />;
19 |
20 | // Matches snapshot
21 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
22 |
23 | // Has the correct text on the page
24 | const { getByText } = render(Component);
25 | expect(getByText('hello boy'));
26 | expect(getByText('Try Again'));
27 | });
28 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/components/UI/Header.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { render } from '@testing-library/react-native';
4 | import Header from '../../../components/UI/Header';
5 |
6 | it(' renders with message', () => {
7 | const Component = ;
8 |
9 | // Matches snapshot
10 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
11 |
12 | // Has the correct text on the page
13 | const { getByText } = render(Component);
14 | expect(getByText('hello boy'));
15 | expect(getByText("I'm here"));
16 | });
17 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/components/UI/Loading.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import Loading from '../../../components/UI/Loading';
4 |
5 | it(' renders correctly', () => {
6 | const Component = ;
7 |
8 | // Matches snapshot
9 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
10 | });
11 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/components/UI/Messages.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { render } from '@testing-library/react-native';
4 | import Messages from '../../../components/UI/Messages';
5 |
6 | it(' renders with error and message', () => {
7 | const Component = ;
8 |
9 | // Matches snapshot
10 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
11 |
12 | // Has the correct text on the page
13 | const { getByText } = render(Component);
14 | expect(getByText('Success'));
15 | });
16 |
17 | it(' renders with error and message', () => {
18 | const Component = ;
19 |
20 | // Matches snapshot
21 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
22 |
23 | // Has the correct text on the page
24 | const { getByText } = render(Component);
25 | expect(getByText('Error'));
26 | });
27 |
28 | it(' renders with info and message', () => {
29 | const Component = ;
30 |
31 | // Matches snapshot
32 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
33 |
34 | // Has the correct text on the page
35 | const { getByText } = render(Component);
36 | expect(getByText('Warning'));
37 | });
38 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/components/UI/Spacer.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import Spacer from '../../../components/UI/Spacer';
4 |
5 | it(' renders with correctly with size: 10', () => {
6 | const Component = ;
7 |
8 | // Matches snapshot
9 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
10 | });
11 |
12 | it(' renders with correctly with size: 15', () => {
13 | const Component = ;
14 |
15 | // Matches snapshot
16 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
17 | });
18 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/components/UI/__snapshots__/Error.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders with a button 1`] = `
4 |
18 |
28 |
39 |
53 | hello boy
54 |
55 |
71 | An unexpected error came up
72 |
73 |
110 |
125 | Try Again
126 |
127 |
128 |
139 |
140 |
141 | `;
142 |
143 | exports[` renders with message 1`] = `
144 |
158 |
168 |
179 |
193 | hello boy
194 |
195 |
211 | An unexpected error came up
212 |
213 |
224 |
225 |
226 | `;
227 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/components/UI/__snapshots__/Header.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders with message 1`] = `
4 |
5 |
16 |
25 | hello boy
26 |
27 |
28 |
39 |
49 | I'm here
50 |
51 |
52 |
63 |
64 | `;
65 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/components/UI/__snapshots__/Loading.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly 1`] = `
4 |
13 |
19 |
20 | `;
21 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/components/UI/__snapshots__/Messages.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders with error and message 1`] = `
4 |
13 |
29 | Success
30 |
31 |
32 | `;
33 |
34 | exports[` renders with error and message 2`] = `
35 |
44 |
60 | Error
61 |
62 |
63 | `;
64 |
65 | exports[` renders with info and message 1`] = `
66 |
75 |
91 | Warning
92 |
93 |
94 | `;
95 |
--------------------------------------------------------------------------------
/OURS-NATIVE/src/tests/components/UI/__snapshots__/Spacer.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders with correctly with size: 10 1`] = `
4 |
15 | `;
16 |
17 | exports[` renders with correctly with size: 15 1`] = `
18 |
29 | `;
30 |
--------------------------------------------------------------------------------
/OURS-WEB/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Github Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-18.04
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Setup Node
15 | uses: actions/setup-node@v1
16 | with:
17 | node-version: '12.x'
18 |
19 | - name: Get yarn cache
20 | id: yarn-cache
21 | run: echo "::set-output name=dir::$(yarn cache dir)"
22 |
23 | - name: Cache dependencies
24 | uses: actions/cache@v1
25 | with:
26 | path: ${{ steps.yarn-cache.outputs.dir }}
27 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
28 | restore-keys: |
29 | ${{ runner.os }}-yarn-
30 |
31 | - run: yarn install
32 | - run: yarn build
33 |
34 | - name: Deploy
35 | uses: peaceiris/actions-gh-pages@v3
36 | with:
37 | github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
38 | publish_dir: ./build
39 |
--------------------------------------------------------------------------------
/OURS-WEB/README.md:
--------------------------------------------------------------------------------
1 |
28 |
29 | ---
30 |
31 | ### Looking for something else?
32 |
33 | - [React Native Starter Kit / Boilerplate](https://github.com/mcnamee/react-native-starter-kit)
34 | - [React Native Starter Kit (Expo) / Boilerplate](https://github.com/mcnamee/react-native-expo-starter-kit)
35 | - [Previous Version (React Starter Kit (Web + Native) w/ Firebase)](https://github.com/mcnamee/react-native-starter-kit/tree/archive/v3)
36 |
37 | ---
38 |
39 | ## 👋 Intro
40 |
41 | This project was bootstrapped with the [React Boilerplate Builder](https://github.com/mcnamee/react-native-boilerplate-builder) by [Matt McNamee](https://mcnam.ee).
42 |
43 | The project is _super_ helpful to kick-start your next project, as it provides a lot of the common tools you may reach for, all ready to go. Specifically:
44 |
45 | - __Flux architecture__
46 | - [Redux](https://redux.js.org/docs/introduction/)
47 | - Redux Wrapper: [Rematch](https://github.com/rematch/rematch)
48 | - __Routing and navigation__
49 | - [React Router](https://github.com/ReactTraining/react-router) for web
50 | - __Data Caching / Offline__
51 | - [Redux Persist](https://github.com/rt2zz/redux-persist)
52 | - __UI Toolkit__
53 | - [Bootstrap](https://getbootstrap.com/) for web
54 | - __Code Linting__ with
55 | - [Airbnb's JS Linting](https://github.com/airbnb/javascript) guidelines
56 |
57 | ---
58 |
59 | ## 🚀 Getting Started
60 |
61 | - Install `eslint`, `prettier` and `editor config` plugins into your IDE
62 |
63 | ```bash
64 | # Install dependencies
65 | yarn install
66 |
67 | # Run the app in the development mode
68 | # Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
69 | yarn start
70 |
71 | # Launches the test runner in the interactive watch mode
72 | yarn test
73 |
74 | # Builds the app for production to the `build` folder
75 | # It correctly bundles React in production mode and optimizes the build for the best performance
76 | yarn build
77 |
78 | # Lint
79 | ./node_modules/.bin/eslint "src/**/*.js"
80 | ```
81 |
82 | ---
83 |
84 | ## 📖 Docs
85 |
86 | - [Contributing to this project](documentation/contributing.md)
87 | - [Tests & testing](documentation/testing.md)
88 | - [Understanding the file structure](documentation/file-structure.md)
89 | - [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started)
90 | - [React documentation](https://reactjs.org/)
91 | - [Code Splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
92 | - [Analyzing the Bundle Size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
93 | - [Making a Progressive Web App](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
94 | - [Advanced Configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
95 | - [Deployment](https://facebook.github.io/create-react-app/docs/deployment)
96 | - [`yarn run build` fails to minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
97 |
98 | ---
99 |
100 | ## 👊 Further Help?
101 |
102 | This repo is a great place to start. But...if you'd prefer to sit back and have your new project built for you or just need some consultation, [get in touch with me directly](https://mcnam.ee) and I can organise a quote.
103 |
--------------------------------------------------------------------------------
/OURS-WEB/documentation/rsk-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-WEB/documentation/rsk-logo.jpg
--------------------------------------------------------------------------------
/OURS-WEB/documentation/web-demo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-WEB/documentation/web-demo.jpg
--------------------------------------------------------------------------------
/OURS-WEB/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Single Page Apps for GitHub Pages
6 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/OURS-WEB/public/CNAME:
--------------------------------------------------------------------------------
1 | react-starter-kit.mcnam.ee
2 |
--------------------------------------------------------------------------------
/OURS-WEB/src/assets/images/app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS-WEB/src/assets/images/app-icon.png
--------------------------------------------------------------------------------
/OURS-WEB/src/assets/styles/_bootstrap.scss:
--------------------------------------------------------------------------------
1 | // Override Bootstrap's variables before importing
2 | // node_modules/bootstrap/scss/_variables.scss
3 |
4 | // Breakpoints
5 | $grid-breakpoints: (
6 | xs: 0,
7 | sm: 576px,
8 | md: 768px,
9 | lg: 992px,
10 | xl: 1200px,
11 | xxl: 1400px
12 | );
13 |
14 | // Jumbotron
15 | $jumbotron-bg: white;
16 |
17 | // Misc
18 | $border-radius: 3px;
19 |
20 | // Buttons
21 | $btn-border-radius: 3px;
22 | $btn-padding-x: 25px;
23 | $btn-padding-x-lg: 50px;
24 | $btn-font-size-lg: 1.15rem;
25 |
26 | // Type
27 | $headings-margin-bottom: 0.5em;
28 | $h1-font-size: 32px;
29 | $h2-font-size: 24px;
30 | $h3-font-size: 20px;
31 | $h4-font-size: 16px;
32 |
33 | $display1-size: 50px;
34 | $display2-size: 34px;
35 | $display3-size: 24px;
36 | $display4-size: 16px;
37 |
38 | // Then import Bootstrap
39 | @import "node_modules/bootstrap/scss/bootstrap";
40 |
--------------------------------------------------------------------------------
/OURS-WEB/src/assets/styles/components/_footer.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Styles for the Footer.js component
3 | */
4 |
--------------------------------------------------------------------------------
/OURS-WEB/src/assets/styles/components/_forms.scss:
--------------------------------------------------------------------------------
1 | label.required {
2 | &:after {
3 | position: relative;
4 | top: -2px;
5 | right: -2px;
6 | content: "*";
7 | display: inline;
8 | color: red;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/OURS-WEB/src/assets/styles/components/_header.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Styles for the Header.js component
3 | */
4 |
--------------------------------------------------------------------------------
/OURS-WEB/src/assets/styles/components/_mobile-tab-bar.scss:
--------------------------------------------------------------------------------
1 | .mobile-tab-bar {
2 | position: fixed;
3 | bottom: 0;
4 | left: 0;
5 | right: 0;
6 | background: $light;
7 | z-index: 99;
8 | box-shadow: $box-shadow;
9 | text-align: center;
10 | border-top: 1px solid $border-color;
11 |
12 | .nav-pills {
13 | .nav-link {
14 | color: $gray-500;
15 | border-radius: 0;
16 | padding-left: 0.4rem;
17 | padding-right: 0.4rem;
18 | padding-bottom: 25px;
19 | border-top: 3px solid transparent;
20 |
21 | svg {
22 | color: $gray-800;
23 | display: block;
24 | margin: 0 auto 5px;
25 | font-size: 1.4rem;
26 | }
27 |
28 | span {
29 | display: block;
30 | font-size: 0.6rem;
31 | }
32 |
33 | &.active {
34 | background: transparent;
35 | color: #2469F6;
36 | border-top: 3px solid #2469F6;
37 |
38 | svg {
39 | color: #2469F6;
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/OURS-WEB/src/assets/styles/components/_tables.scss:
--------------------------------------------------------------------------------
1 | table {
2 | tbody {
3 | tr:first-of-type {
4 | td {
5 | border-top: none;
6 | }
7 | }
8 | }
9 | }
10 |
11 | /* Adds a right hand shadow on the table */
12 | .table-responsive {
13 | @include media-breakpoint-down(sm) {
14 | position: relative;
15 | display: flex;
16 |
17 | &:after {
18 | content: '';
19 | display: block;
20 | position: sticky;
21 | width: 20px;
22 | background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.25) 100%);
23 | margin-left: -20px;
24 | z-index: 2;
25 | pointer-events: none;
26 | flex-shrink: 0;
27 | right: 0;
28 | }
29 | }
30 | }
31 |
32 | table {
33 | thead {
34 | th {
35 | font-size: 0.8rem;
36 | text-transform: uppercase;
37 | letter-spacing: 1px;
38 | color: $gray-600;
39 | }
40 | }
41 | }
42 |
43 | /* Hides all columns except the first */
44 | table.mobile-list-table {
45 | @include media-breakpoint-down(sm) {
46 | th, td {
47 | display: none;
48 | }
49 |
50 | td.mobile-list-table-keep {
51 | display: table-cell;
52 | position: relative;
53 | cursor: pointer;
54 |
55 | &:before {
56 | content:"\203A";
57 | color: $gray-400;
58 | font-size: 2rem;
59 | position: absolute;
60 | right: 10px;
61 | top: 50%;
62 | transform: translateY(-56%);
63 | }
64 | }
65 | }
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/OURS-WEB/src/assets/styles/style.scss:
--------------------------------------------------------------------------------
1 | @import "bootstrap";
2 |
3 | /**
4 | * Import Components
5 | */
6 | @import "components/header";
7 | @import "components/forms";
8 | @import "components/footer";
9 | @import "components/mobile-tab-bar";
10 | @import "components/tables";
11 |
12 | /**
13 | * Backgrounds
14 | */
15 | .bg-gray-100 { background-color: $gray-100; }
16 | .bg-gray-200 { background-color: $gray-200; }
17 | .bg-gray-300 { background-color: $gray-300; }
18 | .bg-gray-400 { background-color: $gray-400; }
19 | .bg-gray-500 { background-color: $gray-500; }
20 | .bg-gray-600 { background-color: $gray-600; }
21 | .bg-gray-700 { background-color: $gray-700; }
22 | .bg-gray-800 { background-color: $gray-800; }
23 | .bg-gray-900 { background-color: $gray-900; }
24 |
25 | /**
26 | * Generic Styles
27 | */
28 | body, html {
29 | height: 100%;
30 | width: 100%;
31 | display: block;
32 |
33 | * {
34 | outline: none !important;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/About.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Row, Col, Jumbotron, Container,
4 | } from 'reactstrap';
5 | import { Link, withRouter } from 'react-router-dom';
6 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
7 | import {
8 | faTachometerAlt, faPalette, faMoneyBillWave, faCertificate, faPlus, faUserCircle,
9 | } from '@fortawesome/free-solid-svg-icons';
10 | import Template from './Templates/Dashboard';
11 |
12 | const About = () => (
13 |
14 |
15 |
16 |
17 |
18 | Lorem Ipsum is simply dummy text.
19 |
20 | Lorem Ipsum is simply dummy text of the printing and typesetting industry.
21 | Lorem Ipsum has been the industry's standard dummy text ever since the 1500s.
22 |
23 |
24 | Lorem Ipsum is simply dummy text of the printing and typesetting industry.
25 | Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an
26 | unknown printer took a galley of type and scrambled it to make a type specimen book.
27 | It has survived not only five centuries.
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {' '}
36 | Lorem Ipsum
37 |
38 | Lorem Ipsum is simply dummy text of the printing and typesetting industry.
39 | Learn More
40 |
41 |
42 |
43 |
44 | {' '}
45 | Lorem Ipsum
46 |
47 | Lorem Ipsum is simply dummy text of the printing and typesetting industry.
48 | Learn More
49 |
50 |
51 |
52 |
53 | {' '}
54 | Lorem Ipsum
55 |
56 | Lorem Ipsum is simply dummy text of the printing and typesetting industry.
57 | Learn More
58 |
59 |
60 |
61 |
62 |
63 |
64 | {' '}
65 | Lorem Ipsum
66 |
67 | Lorem Ipsum is simply dummy text of the printing and typesetting industry.
68 | Learn More
69 |
70 |
71 |
72 |
73 | {' '}
74 | Lorem Ipsum
75 |
76 | Lorem Ipsum is simply dummy text of the printing and typesetting industry.
77 | Learn More
78 |
79 |
80 |
81 |
82 | {' '}
83 | Lorem Ipsum
84 |
85 | Lorem Ipsum is simply dummy text of the printing and typesetting industry.
86 | Learn More
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | I can help
100 |
101 | This repo is a great place to start, but if you'd prefer to sit back and have your new
102 | project built for you,
103 | {' '}
104 |
105 | get in touch with me directly
106 |
107 | {' '}
108 | and I'll provide a quote.
109 |
110 |
111 |
112 |
113 |
114 |
115 | );
116 |
117 | export default withRouter(About);
118 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/Articles/Form.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useForm } from 'react-hook-form';
4 | import { withRouter } from 'react-router-dom';
5 | import {
6 | Container,
7 | Row,
8 | Col,
9 | Card,
10 | CardBody,
11 | Alert,
12 | Form,
13 | FormGroup,
14 | Label,
15 | Input,
16 | Button,
17 | } from 'reactstrap';
18 | import Template from '../Templates/Dashboard';
19 | import { errorMessages } from '../../constants/messages';
20 |
21 | const ArticlesForm = ({
22 | error, loading, success, onFormSubmit, defaultValues,
23 | }) => {
24 | const {
25 | register, handleSubmit, errors, setValue,
26 | } = useForm({ defaultValues });
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | );
67 | };
68 |
69 | ArticlesForm.propTypes = {
70 | error: PropTypes.string,
71 | loading: PropTypes.bool,
72 | success: PropTypes.string,
73 | defaultValues: PropTypes.shape({
74 | email: PropTypes.string,
75 | }),
76 | onFormSubmit: PropTypes.func.isRequired,
77 | };
78 |
79 | ArticlesForm.defaultProps = {
80 | error: null,
81 | success: null,
82 | loading: false,
83 | defaultValues: {},
84 | };
85 |
86 | export default withRouter(ArticlesForm);
87 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/Articles/List.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { withRouter, Link } from 'react-router-dom';
4 | import {
5 | Container,
6 | Row,
7 | Col,
8 | Table,
9 | Card,
10 | CardBody,
11 | Alert,
12 | } from 'reactstrap';
13 | import Template from '../Templates/Dashboard';
14 | import TablePagination from '../UI/TablePagination';
15 |
16 | const List = ({
17 | error, loading, listPaginated, page, pagination, meta, history,
18 | }) => (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {!!error && {error} }
27 | {(!!loading && (!listPaginated[page] || listPaginated[page].length === 0)) && (
28 | Loading...
29 | )}
30 |
31 |
37 |
38 | {(listPaginated[page] && listPaginated[page].length > 0) && (
39 |
40 |
41 |
42 | Title
43 | Date Posted
44 | {' '}
45 |
46 |
47 |
48 | {listPaginated[page].map((article) => (
49 |
50 | { /* eslint-disable-next-line */ }
51 | history.push(`/article/${article.id}`)}
54 | >
55 | {article.name}
56 |
57 | {article.date}
58 |
59 |
65 | View
66 |
67 |
68 |
69 | ))}
70 |
71 |
72 | )}
73 |
74 |
80 |
81 |
82 | {(!loading && (!listPaginated[page] || listPaginated[page].length === 0)) && (
83 |
No Articles found
84 | )}
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | );
94 |
95 | List.propTypes = {
96 | error: PropTypes.string,
97 | loading: PropTypes.bool,
98 | listPaginated: PropTypes.shape({}),
99 | pagination: PropTypes.arrayOf(PropTypes.shape({
100 | title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
101 | link: PropTypes.string.isRequired,
102 | })),
103 | page: PropTypes.number.isRequired,
104 | meta: PropTypes.shape({ total: PropTypes.number }),
105 | history: PropTypes.shape({
106 | push: PropTypes.func.isRequired,
107 | }).isRequired,
108 | };
109 |
110 | List.defaultProps = {
111 | error: null,
112 | listPaginated: {},
113 | loading: false,
114 | pagination: [],
115 | meta: { total: 0 },
116 | };
117 |
118 | export default withRouter(List);
119 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/Articles/Single.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { withRouter, Link, Redirect } from 'react-router-dom';
4 | import {
5 | Container,
6 | Row,
7 | Col,
8 | Card,
9 | CardBody,
10 | Alert,
11 | CardImg,
12 | } from 'reactstrap';
13 | import Template from '../Templates/Dashboard';
14 |
15 | const Single = ({
16 | error, loading, article,
17 | }) => {
18 | if (!loading && !article) return ;
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 | {!!error && {error} }
27 | {!!loading && Loading... }
28 |
29 |
30 | {article.id && (
31 |
32 |
33 | {article.image && (
34 |
35 | )}
36 |
37 | Posted
38 | {' '}
39 | {article.date ? article.date : 'n/a'}
40 |
41 | {/* eslint-disable-next-line */}
42 | {article.contentRaw &&
}
43 |
44 |
45 |
46 | )}
47 |
48 |
49 |
50 |
51 | Back
52 |
53 |
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | Single.propTypes = {
61 | error: PropTypes.string,
62 | loading: PropTypes.bool,
63 | article: PropTypes.shape({
64 | placeholder: PropTypes.bool,
65 | id: PropTypes.number,
66 | name: PropTypes.string,
67 | date: PropTypes.string,
68 | content: PropTypes.string,
69 | contentRaw: PropTypes.string,
70 | excerpt: PropTypes.string,
71 | image: PropTypes.string,
72 | }),
73 | };
74 |
75 | Single.defaultProps = {
76 | error: null,
77 | loading: false,
78 | article: {},
79 | };
80 |
81 | export default withRouter(Single);
82 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/Templates/Dashboard.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Helmet } from 'react-helmet';
4 | import Header from '../UI/Header';
5 | import MobileTabBar from '../UI/MobileTabBar';
6 | import Footer from '../UI/Footer';
7 | import PageTitle from '../UI/PageTitle';
8 |
9 | const Template = ({ pageTitle, children, noPadding }) => (
10 |
11 |
12 | {pageTitle}
13 |
14 |
15 |
16 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | );
24 |
25 | Template.propTypes = {
26 | pageTitle: PropTypes.string,
27 | children: PropTypes.element.isRequired,
28 | noPadding: PropTypes.bool,
29 | };
30 |
31 | Template.defaultProps = {
32 | pageTitle: 'AwesomeProject',
33 | noPadding: false,
34 | };
35 |
36 | export default Template;
37 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/Templates/Nothing.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Container, Row, Col } from 'reactstrap';
4 | import { Helmet } from 'react-helmet';
5 | import { Link, withRouter } from 'react-router-dom';
6 | import Footer from '../UI/Footer';
7 | import Logo from '../../assets/images/logo.png';
8 |
9 | const Template = ({ pageTitle, children }) => (
10 |
11 |
12 |
13 |
14 | {pageTitle}
15 |
16 |
17 |
18 |
19 |
20 |
26 |
27 |
28 |
29 |
30 | {children}
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 |
39 | Template.propTypes = {
40 | pageTitle: PropTypes.string,
41 | children: PropTypes.element.isRequired,
42 | };
43 |
44 | Template.defaultProps = {
45 | pageTitle: 'AwesomeProject',
46 | };
47 |
48 | export default withRouter(Template);
49 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/UI/Error.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Row, Col } from 'reactstrap';
4 | // import { Link } from 'react-router-dom';
5 |
6 | const Error = ({ title, content }) => (
7 |
8 |
9 | {title}
10 | {content}
11 | {/* Go Home
*/}
12 |
13 |
14 | );
15 |
16 | Error.propTypes = {
17 | title: PropTypes.string,
18 | content: PropTypes.string,
19 | };
20 |
21 | Error.defaultProps = {
22 | title: 'Uh oh',
23 | content: 'An unexpected error came up',
24 | };
25 |
26 | export default Error;
27 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/UI/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Container, Row, Col } from 'reactstrap';
3 |
4 | const Footer = () => (
5 |
6 |
7 |
8 |
9 | © AwesomeProject. All Rights Reserved.
10 |
11 |
12 |
13 |
14 | );
15 |
16 | export default Footer;
17 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/UI/Header.js:
--------------------------------------------------------------------------------
1 | /* global window */
2 | import React, { useState } from 'react';
3 | import {
4 | Nav,
5 | Navbar,
6 | NavItem,
7 | Collapse,
8 | NavbarToggler,
9 | } from 'reactstrap';
10 | import { Link } from 'react-router-dom';
11 | import Config from '../../constants/config';
12 |
13 | const Header = () => {
14 | const [isOpen, setIsOpen] = useState(false);
15 |
16 | return (
17 |
18 |
19 |
20 | {/* eslint-disable */}
21 |
22 |
23 |
24 |
25 |
26 | {/* eslint-enable */}
27 | {Config.appName}
28 |
29 |
30 | setIsOpen(!isOpen)} />
31 |
32 |
33 |
34 |
35 |
36 | Home
37 |
38 |
39 |
40 |
44 | Articles
45 |
46 |
47 |
48 |
52 | Form
53 |
54 |
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | export default Header;
63 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/UI/Loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Row, Col, Progress } from 'reactstrap';
3 |
4 | const Loading = () => (
5 |
6 |
7 |
10 |
11 |
12 | );
13 |
14 | export default Loading;
15 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/UI/MobileTabBar.js:
--------------------------------------------------------------------------------
1 | /* global window */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import {
5 | Nav,
6 | NavItem,
7 | } from 'reactstrap';
8 | import { Link, withRouter } from 'react-router-dom';
9 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
10 | import { faTachometerAlt, faPalette, faMoneyBillWave } from '@fortawesome/free-solid-svg-icons';
11 |
12 | const MobileTabBar = () => (
13 |
14 |
15 |
16 |
17 |
18 | Home
19 |
20 |
21 |
22 |
26 |
27 | Articles
28 |
29 |
30 |
31 |
35 |
36 | Form
37 |
38 |
39 |
40 |
41 | );
42 |
43 | MobileTabBar.propTypes = {
44 | history: PropTypes.shape({
45 | push: PropTypes.func.isRequired,
46 | }).isRequired,
47 | };
48 |
49 | export default withRouter(MobileTabBar);
50 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/UI/Notice.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Row, Col, Alert } from 'reactstrap';
4 |
5 | const Notice = ({
6 | title, content, error, success, loading, padding,
7 | }) => (
8 |
9 |
10 | {title && {title} }
11 | {!!success && {success} }
12 | {!!error && {error} }
13 | {!!loading && Loading... }
14 | {content}
15 |
16 |
17 | );
18 |
19 | Notice.propTypes = {
20 | title: PropTypes.string,
21 | content: PropTypes.string,
22 | error: PropTypes.string,
23 | success: PropTypes.string,
24 | loading: PropTypes.bool,
25 | padding: PropTypes.bool,
26 | };
27 |
28 | Notice.defaultProps = {
29 | title: '',
30 | content: '',
31 | error: null,
32 | success: null,
33 | loading: false,
34 | padding: true,
35 | };
36 |
37 | export default Notice;
38 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/UI/PageTitle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Container, Jumbotron } from 'reactstrap';
4 |
5 | const PageTitle = ({ title }) => (
6 |
7 |
8 | {title}
9 |
10 |
11 | );
12 |
13 | PageTitle.propTypes = {
14 | title: PropTypes.string,
15 | };
16 |
17 | PageTitle.defaultProps = {
18 | title: '',
19 | };
20 |
21 | export default PageTitle;
22 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/UI/TablePagination.js:
--------------------------------------------------------------------------------
1 | /* global window */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import { Link } from 'react-router-dom';
5 | import { Pagination, PaginationItem } from 'reactstrap';
6 |
7 | const Component = ({
8 | pagination, length, total, loading,
9 | }) => (
10 |
11 | {(pagination && pagination.length > 0 && !loading) && (
12 |
13 |
{`Showing ${length} of ${total}`}
14 |
15 |
16 | {pagination.map((page) => (
17 |
21 | {page.title}
22 |
23 | ))}
24 |
25 |
26 | )}
27 |
28 | );
29 |
30 | Component.propTypes = {
31 | pagination: PropTypes.arrayOf(PropTypes.shape({
32 | title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
33 | link: PropTypes.string.isRequired,
34 | })),
35 | length: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
36 | total: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
37 | loading: PropTypes.bool,
38 | };
39 |
40 | Component.defaultProps = {
41 | pagination: [],
42 | length: 0,
43 | total: 0,
44 | loading: false,
45 | };
46 |
47 | export default Component;
48 |
--------------------------------------------------------------------------------
/OURS-WEB/src/components/UI/index.js:
--------------------------------------------------------------------------------
1 | export { default as Error } from './Error';
2 | export { default as Header } from './Header';
3 | export { default as Loading } from './Loading';
4 | export { default as Messages } from './Messages';
5 | export { default as Spacer } from './Spacer';
6 |
--------------------------------------------------------------------------------
/OURS-WEB/src/constants/config.js:
--------------------------------------------------------------------------------
1 | /* global window */
2 | const { host } = window.location;
3 |
4 | /**
5 | * Environments
6 | */
7 | let env = 'production'; // Defaults to production
8 | if (process.env.NODE_ENV === 'development' || host.includes('local')) env = 'development';
9 | if (host.includes('stage.')) env = 'stage';
10 |
11 | /**
12 | * Config object to export
13 | */
14 | export default {
15 | // App Details
16 | appName: 'AwesomeProject',
17 |
18 | // Build Configuration - eg. Debug or Release?
19 | isDevEnv: (env === 'development'),
20 | ENV: env,
21 |
22 | // Date Format
23 | dateFormat: 'Do MMM YYYY',
24 |
25 | // API
26 | apiBaseUrl: (env === 'production')
27 | ? 'https://digitalsupply.co/wp-json/wp'
28 | : 'https://digitalsupply.co/wp-json/wp',
29 |
30 | // Google Analytics - uses a 'dev' account while we're testing
31 | gaTrackingId: (env === 'production') ? 'UA-84284256-2' : 'UA-84284256-1',
32 | };
33 |
--------------------------------------------------------------------------------
/OURS-WEB/src/index.js:
--------------------------------------------------------------------------------
1 | /* global document */
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import { Provider } from 'react-redux';
5 | import { BrowserRouter as Router } from 'react-router-dom';
6 | import { PersistGate } from 'redux-persist/es/integration/react';
7 |
8 | import configureStore from './store/index';
9 | import * as serviceWorker from './lib/service-worker';
10 | import Routes from './routes/index';
11 |
12 | // Components
13 | import Loading from './components/UI/Loading';
14 |
15 | // Load css
16 | import './assets/styles/style.scss';
17 |
18 | const { persistor, store, dispatch } = configureStore();
19 | // persistor.purge(); // Debug to clear persist
20 |
21 | const Root = () => (
22 |
23 | } persistor={persistor}>
24 |
25 |
26 |
27 |
28 |
29 | );
30 |
31 | ReactDOM.render( , document.getElementById('root'));
32 |
33 | // If you want your app to work offline and load faster, you can change
34 | // unregister() to register() below. Note this comes with some pitfalls.
35 | // Learn more about service workers: http://bit.ly/CRA-PWA
36 | serviceWorker.unregister();
37 |
--------------------------------------------------------------------------------
/OURS-WEB/src/lib/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { getCookie } from './cookies';
3 | import Config from '../constants/config';
4 | import { hasActiveAuthToken, hasAuthToken, refreshAuthToken } from './jwt';
5 |
6 | /**
7 | * Axios defaults
8 | */
9 | axios.defaults.baseURL = Config.apiBaseUrl;
10 | // axios.defaults.withCredentials = true;
11 |
12 | // Headers
13 | axios.defaults.headers.common['Content-Type'] = 'application/json';
14 | axios.defaults.headers.common.Accept = 'application/json';
15 |
16 | /**
17 | * Request Interceptor
18 | */
19 | axios.interceptors.request.use(
20 | async (inputConfig) => {
21 | const config = inputConfig;
22 |
23 | try {
24 | // Check if the token has or is about to expire, and refresh it
25 | if (hasActiveAuthToken()) {
26 | // Add the token to the Authorization header
27 | config.headers.common.Authorization = `Bearer ${getCookie('Auth:Token')}`;
28 |
29 | // Otherwise, attempt to refresh the token
30 | } else if (hasAuthToken()) {
31 | const token = await refreshAuthToken();
32 |
33 | if (token) {
34 | config.headers.common.Authorization = `Bearer ${token}`;
35 | }
36 | }
37 | } catch (error) { /* Nothing */ }
38 |
39 | return config;
40 | },
41 | (error) => {
42 | throw error;
43 | },
44 | );
45 |
46 | /**
47 | * Response Interceptor
48 | */
49 | axios.interceptors.response.use(
50 | (res) => {
51 | // Status code isn't a success code - throw error
52 | if (!`${res.status}`.startsWith('2')) {
53 | throw res.data;
54 | }
55 |
56 | // Otherwise just return the data
57 | return res;
58 | },
59 | (error) => {
60 | // Pass the response from the API, rather than a status code
61 | if (error && error.response && error.response.data) {
62 | throw error.response.data;
63 | }
64 | throw error;
65 | },
66 | );
67 |
68 | export default axios;
69 |
--------------------------------------------------------------------------------
/OURS-WEB/src/lib/cookies.js:
--------------------------------------------------------------------------------
1 | /* global document */
2 |
3 | /**
4 | * Create a new Cookie
5 | * @param {str} cookieName
6 | * @param {str} cookieValue
7 | * @param {int} expireInDays
8 | */
9 | export const setCookie = (cookieName, cookieValue, expireInDays) => {
10 | const d = new Date();
11 | d.setTime(d.getTime() + (expireInDays * 24 * 60 * 60 * 1000));
12 | const expires = `expires=${d.toUTCString()}`;
13 |
14 | document.cookie = `${cookieName}=${cookieValue};${expires};path=/`;
15 | };
16 |
17 | /**
18 | * Get the value of a Cookie
19 | * @param {str} cookieName
20 | */
21 | export const getCookie = (cookieName) => {
22 | const value = `; ${document.cookie}`;
23 | const parts = value.split(`; ${cookieName}=`);
24 |
25 | if (parts.length === 2) return parts.pop().split(';').shift();
26 |
27 | return false;
28 | };
29 |
30 | /**
31 | * Deletes a Cookie by cookie name (expires it)
32 | * @param {str} cookieName
33 | */
34 | export const deleteCookie = (cookieName) => {
35 | document.cookie = `${cookieName}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
36 | };
37 |
--------------------------------------------------------------------------------
/OURS-WEB/src/lib/jwt.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import jwt from 'jsonwebtoken';
3 | import moment from 'moment';
4 | import { getCookie, setCookie, deleteCookie } from './cookies';
5 | import { errorMessages } from '../constants/messages';
6 |
7 | /**
8 | * Does the current user have a valid and active JWT Auth Token?
9 | * - We'll test by getting the auth token from the cookie
10 | * - and decoding to see if it's still active
11 | * @return {bool}
12 | */
13 | export const hasActiveAuthToken = () => {
14 | // Does a token exist?
15 | const token = getCookie('Auth:Token') || null;
16 | if (!token) {
17 | return false;
18 | }
19 |
20 | // Is the token a valid JWT token?
21 | try {
22 | const decoded = jwt.decode(token);
23 |
24 | if (decoded.exp) {
25 | const expires = moment.unix(decoded.exp);
26 | const today = moment();
27 |
28 | // Expires in the future, so by all accounts, we're authenticated
29 | if (expires.diff(today) > 0) {
30 | return true;
31 | }
32 | }
33 | } catch (error) { return false; }
34 |
35 | return false;
36 | };
37 |
38 | /**
39 | * Does the current user have an Auth Token set?
40 | * - We use this primarily for routes.
41 | * - We don't use hasActiveAuthToken for routes, because we don't want the app to
42 | * - kick them out before the API request refreshes the token
43 | * - (token refreshes are handled by an API interceptor)
44 | * @return {bool}
45 | */
46 | export const hasAuthToken = () => {
47 | // Does a token exist?
48 | const token = getCookie('Auth:Token') || null;
49 | if (!token) {
50 | return false;
51 | }
52 |
53 | // Is the token a valid JWT token? (does it decode and have an expiry?)
54 | try {
55 | const decoded = jwt.decode(token);
56 |
57 | if (decoded.exp) {
58 | return true;
59 | }
60 | } catch (error) { return false; }
61 |
62 | return false;
63 | };
64 |
65 | /**
66 | * Refresh a token (be-it expired or not)
67 | * @return {str} the token
68 | */
69 | export const refreshAuthToken = async () => {
70 | // Does a token exist?
71 | const token = getCookie('Auth:Token') || null;
72 | if (!token) {
73 | return false;
74 | }
75 |
76 | // Delete the cookie so that future requests don't attempt to use it
77 | // - which may cause an infinite loop
78 | // - (i.e. refresh token request checking for token and trying to refresh and on and on...)
79 | deleteCookie('Auth:Token');
80 |
81 | // Is the token a valid JWT token? (does it decode and have an expiry?)
82 | try {
83 | const response = await axios.post('/v2/refresh-auth', { token });
84 |
85 | const { data } = response;
86 | if (!response || !data || !data.token) {
87 | throw new Error(errorMessages.memberNotAuthd);
88 | }
89 |
90 | // Save the token to a cookie
91 | setCookie('Auth:Token', data.token, 30);
92 |
93 | return data.token;
94 | } catch (error) { return false; }
95 | };
96 |
--------------------------------------------------------------------------------
/OURS-WEB/src/lib/service-worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | // This optional code is used to register a service worker.
3 | // register() is not called by default.
4 |
5 | // This lets the app load faster on subsequent visits in production, and gives
6 | // it offline capabilities. However, it also means that developers (and users)
7 | // will only see deployed updates on subsequent visits to a page, after all the
8 | // existing tabs open on the page have been closed, since previously cached
9 | // resources are updated in the background.
10 |
11 | // To learn more about the benefits of this model and instructions on how to
12 | // opt-in, read https://bit.ly/CRA-PWA
13 |
14 | const isLocalhost = Boolean(
15 | window.location.hostname === 'localhost' ||
16 | // [::1] is the IPv6 localhost address.
17 | window.location.hostname === '[::1]' ||
18 | // 127.0.0.1/8 is considered localhost for IPv4.
19 | window.location.hostname.match(
20 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
21 | )
22 | );
23 |
24 | export function register(config) {
25 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
26 | // The URL constructor is available in all browsers that support SW.
27 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
28 | if (publicUrl.origin !== window.location.origin) {
29 | // Our service worker won't work if PUBLIC_URL is on a different origin
30 | // from what our page is served on. This might happen if a CDN is used to
31 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
32 | return;
33 | }
34 |
35 | window.addEventListener('load', () => {
36 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
37 |
38 | if (isLocalhost) {
39 | // This is running on localhost. Let's check if a service worker still exists or not.
40 | checkValidServiceWorker(swUrl, config);
41 |
42 | // Add some additional logging to localhost, pointing developers to the
43 | // service worker/PWA documentation.
44 | navigator.serviceWorker.ready.then(() => {
45 | console.log(
46 | 'This web app is being served cache-first by a service ' +
47 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
48 | );
49 | });
50 | } else {
51 | // Is not localhost. Just register service worker
52 | registerValidSW(swUrl, config);
53 | }
54 | });
55 | }
56 | }
57 |
58 | function registerValidSW(swUrl, config) {
59 | navigator.serviceWorker
60 | .register(swUrl)
61 | .then(registration => {
62 | registration.onupdatefound = () => {
63 | const installingWorker = registration.installing;
64 | if (installingWorker == null) {
65 | return;
66 | }
67 | installingWorker.onstatechange = () => {
68 | if (installingWorker.state === 'installed') {
69 | if (navigator.serviceWorker.controller) {
70 | // At this point, the updated precached content has been fetched,
71 | // but the previous service worker will still serve the older
72 | // content until all client tabs are closed.
73 | console.log(
74 | 'New content is available and will be used when all ' +
75 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
76 | );
77 |
78 | // Execute callback
79 | if (config && config.onUpdate) {
80 | config.onUpdate(registration);
81 | }
82 | } else {
83 | // At this point, everything has been precached.
84 | // It's the perfect time to display a
85 | // "Content is cached for offline use." message.
86 | console.log('Content is cached for offline use.');
87 |
88 | // Execute callback
89 | if (config && config.onSuccess) {
90 | config.onSuccess(registration);
91 | }
92 | }
93 | }
94 | };
95 | };
96 | })
97 | .catch(error => {
98 | console.error('Error during service worker registration:', error);
99 | });
100 | }
101 |
102 | function checkValidServiceWorker(swUrl, config) {
103 | // Check if the service worker can be found. If it can't reload the page.
104 | fetch(swUrl)
105 | .then(response => {
106 | // Ensure service worker exists, and that we really are getting a JS file.
107 | const contentType = response.headers.get('content-type');
108 | if (
109 | response.status === 404 ||
110 | (contentType != null && contentType.indexOf('javascript') === -1)
111 | ) {
112 | // No service worker found. Probably a different app. Reload the page.
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister().then(() => {
115 | window.location.reload();
116 | });
117 | });
118 | } else {
119 | // Service worker found. Proceed as normal.
120 | registerValidSW(swUrl, config);
121 | }
122 | })
123 | .catch(() => {
124 | console.log(
125 | 'No internet connection found. App is running in offline mode.'
126 | );
127 | });
128 | }
129 |
130 | export function unregister() {
131 | if ('serviceWorker' in navigator) {
132 | navigator.serviceWorker.ready.then(registration => {
133 | registration.unregister();
134 | });
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/OURS-WEB/src/routes/PrivateRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Route, Redirect } from 'react-router-dom';
4 | import { connect } from 'react-redux';
5 | import { hasAuthToken } from '../lib/jwt';
6 |
7 | const PrivateRoute = ({ ...rest }) => {
8 | const { member } = rest;
9 |
10 | // Not logged in - redirect to /login
11 | if (!hasAuthToken() || !member) {
12 | return ;
13 | }
14 |
15 | // Logged in and verified
16 | return ;
17 | };
18 |
19 | PrivateRoute.propTypes = {
20 | member: PropTypes.shape({}).isRequired,
21 | };
22 |
23 | const mapStateToProps = (state) => ({
24 | member: state.member || {},
25 | });
26 |
27 | const mapDispatchToProps = () => ({});
28 |
29 | export default connect(mapStateToProps, mapDispatchToProps)(PrivateRoute);
30 |
--------------------------------------------------------------------------------
/OURS-WEB/src/routes/Route.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Route } from 'react-router-dom';
4 |
5 | /**
6 | * Custom Route Component, to pass URL params simpler
7 | */
8 | const CustomRoute = ({ ...props }) => (
9 | } />
10 | );
11 |
12 | CustomRoute.propTypes = {
13 | computedMatch: PropTypes.shape({ params: PropTypes.shape({}) }),
14 | };
15 |
16 | CustomRoute.defaultProps = {
17 | computedMatch: { params: {} },
18 | };
19 |
20 | export default CustomRoute;
21 |
--------------------------------------------------------------------------------
/OURS-WEB/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Switch, Route } from 'react-router-dom';
3 | import CustomRoute from './Route';
4 | // import PrivateRoute from './PrivateRoute';
5 |
6 | // Containers
7 | import {
8 | ArticlesList,
9 | ArticlesSingle,
10 | ArticlesForm,
11 | } from '../containers';
12 |
13 | // Components
14 | import About from '../components/About';
15 | import Error from '../components/UI/Error';
16 |
17 | /**
18 | * All of the routes
19 | */
20 | const Index = () => (
21 |
22 |
23 |
24 | {/* Articles */}
25 |
26 |
27 |
28 |
29 | {/* 404 */}
30 | (
32 |
33 | )}
34 | />
35 |
36 | );
37 |
38 | export default Index;
39 |
--------------------------------------------------------------------------------
/OURS-WEB/src/setupTests.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import '@testing-library/jest-dom/extend-expect';
3 | require('snapshot-diff/extend-expect');
4 |
5 | // Ignore warnings such as: "Warning: componentWillMount has been renamed"
6 | console.warn = () => {};
7 |
8 | global.MutationObserver = class {
9 | constructor(callback) {}
10 | disconnect() {}
11 | observe(element, initObject) {}
12 | };
13 |
--------------------------------------------------------------------------------
/OURS-WEB/src/store/index.js:
--------------------------------------------------------------------------------
1 | /* global */
2 | import { init } from '@rematch/core';
3 | import createPersistPlugin, { getPersistor } from '@rematch/persist';
4 | import createLoadingPlugin from '@rematch/loading';
5 | import storage from 'redux-persist/es/storage';
6 | import * as models from '../models';
7 |
8 | // Create plugins
9 | const persistPlugin = createPersistPlugin({
10 | version: 2,
11 | storage,
12 | blacklist: [],
13 | });
14 | const loadingPlugin = createLoadingPlugin({});
15 |
16 | const configureStore = () => {
17 | const store = init({
18 | models,
19 | redux: {
20 | middlewares: [],
21 | },
22 | plugins: [persistPlugin, loadingPlugin],
23 | });
24 |
25 | const persistor = getPersistor();
26 | const { dispatch } = store;
27 |
28 | return { persistor, store, dispatch };
29 | };
30 |
31 | export default configureStore;
32 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/Articles/Form.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { render } from '@testing-library/react';
4 | import { MemoryRouter } from 'react-router-dom';
5 | import Form from '../../../components/Articles/Form';
6 |
7 | it(' shows a form', () => {
8 | const Component = (
9 |
10 |
12 | );
13 |
14 | // Matches snapshot
15 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
16 | });
17 |
18 | it(' shows loading state', () => {
19 | const { asFragment } = render(
20 |
21 | ,
23 | );
24 | const firstRender = asFragment();
25 |
26 | const { asFragment: asFragment2 } = render(
27 |
28 | ,
30 | );
31 |
32 | expect(firstRender).toMatchDiffSnapshot(asFragment2());
33 | });
34 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/Articles/List.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { render } from '@testing-library/react';
4 | import { MemoryRouter } from 'react-router-dom';
5 | import List from '../../../components/Articles/List';
6 |
7 | it('
shows a nice "no quick quote" message', () => {
8 | const Component = (
9 |
10 |
11 |
12 | );
13 |
14 | // Matches snapshot
15 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
16 |
17 | // Has the correct text on the page
18 | const { getByText } = render(Component);
19 | expect(getByText('No Articles found')).toBeInTheDocument();
20 | });
21 |
22 | it('
shows a table when provided', () => {
23 | const Component = (
24 |
25 |
26 |
27 | );
28 |
29 | // Matches snapshot
30 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
31 |
32 | // Has the correct text on the page
33 | const { getByText } = render(Component);
34 | expect(getByText(/TESTINGABC/)).toBeInTheDocument();
35 | expect(getByText(/22nd June/)).toBeInTheDocument();
36 | });
37 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/Articles/Single.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { render } from '@testing-library/react';
4 | import { MemoryRouter, Router } from 'react-router-dom';
5 | import { createMemoryHistory } from 'history';
6 | import Single from '../../../components/Articles/Single';
7 |
8 | it(' redirects to 404 when no quick quote is passed', () => {
9 | const history = createMemoryHistory();
10 | render( );
11 | expect(history.location.pathname).toBe('/');
12 | });
13 |
14 | it(' shows the article when provided', () => {
15 | const Component = (
16 |
17 |
23 |
24 | );
25 |
26 | // Matches snapshot
27 | expect(renderer.create(Component).toJSON()).toMatchSnapshot();
28 |
29 | // Has the correct text on the page
30 | const { getByText } = render(Component);
31 | expect(getByText('TESTINGABC')).toBeInTheDocument();
32 | expect(getByText('Ello')).toBeInTheDocument();
33 | });
34 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/Error.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import renderer from 'react-test-renderer';
4 | import Error from '../../../components/UI/Error';
5 |
6 | it(' renders correctly', () => {
7 | const Component = ;
8 |
9 | // Matches snapshot
10 | const tree = renderer.create(Component).toJSON();
11 | expect(tree).toMatchSnapshot();
12 |
13 | // Has the correct text on the page
14 | const { getByText } = render(Component);
15 | expect(getByText('Hello')).toBeInTheDocument();
16 | expect(getByText('World')).toBeInTheDocument();
17 | });
18 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/Footer.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { MemoryRouter } from 'react-router-dom';
4 | import Footer from '../../../components/UI/Footer';
5 |
6 | it(' renders correctly', () => {
7 | const Component = ;
8 |
9 | // Matches snapshot
10 | const tree = renderer.create(Component).toJSON();
11 | expect(tree).toMatchSnapshot();
12 | });
13 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/Header.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import renderer from 'react-test-renderer';
4 | import { MemoryRouter } from 'react-router-dom';
5 | import Header from '../../../components/UI/Header';
6 | import Config from '../../../constants/config';
7 |
8 | it(' renders correctly', () => {
9 | const Component = (
10 |
11 |
12 |
13 | );
14 |
15 | // Matches snapshot
16 | const tree = renderer.create(Component).toJSON();
17 | expect(tree).toMatchSnapshot();
18 |
19 | // Has the correct text on the page
20 | const { getByText } = render(Component);
21 | expect(getByText(Config.appName)).toBeInTheDocument();
22 | });
23 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/Loading.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { MemoryRouter } from 'react-router-dom';
4 | import Loading from '../../../components/UI/Loading';
5 |
6 | it(' renders correctly', () => {
7 | const Component = ;
8 |
9 | // Matches snapshot
10 | const tree = renderer.create(Component).toJSON();
11 | expect(tree).toMatchSnapshot();
12 | });
13 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/MobileTabBar.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { MemoryRouter } from 'react-router-dom';
4 | import MobileTabBar from '../../../components/UI/MobileTabBar';
5 |
6 | it(' renders correctly', () => {
7 | const Component = (
8 |
9 |
10 |
11 | );
12 |
13 | // Matches snapshot
14 | const tree = renderer.create(Component).toJSON();
15 | expect(tree).toMatchSnapshot();
16 | });
17 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/PageTitle.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import renderer from 'react-test-renderer';
4 | import { MemoryRouter } from 'react-router-dom';
5 | import PageTitle from '../../../components/UI/PageTitle';
6 |
7 | it(' renders correctly', () => {
8 | const Component = (
9 |
10 |
11 |
12 | );
13 |
14 | // Matches snapshot
15 | const tree = renderer.create(Component).toJSON();
16 | expect(tree).toMatchSnapshot();
17 |
18 | // Has the correct text on the page
19 | const { getByText } = render(Component);
20 | expect(getByText('Hello World')).toBeInTheDocument();
21 | });
22 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/TablePagination.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import renderer from 'react-test-renderer';
4 | import { MemoryRouter } from 'react-router-dom';
5 | import TablePagination from '../../../components/UI/TablePagination';
6 |
7 | it(' renders correctly', () => {
8 | const Component = (
9 |
10 |
24 |
25 | );
26 |
27 | // Matches snapshot
28 | const tree = renderer.create(Component).toJSON();
29 | expect(tree).toMatchSnapshot();
30 |
31 | // Has the correct text on the page
32 | const { getByText } = render(Component);
33 | expect(getByText('Showing 6 of 12')).toBeInTheDocument();
34 | expect(getByText('2')).toBeInTheDocument();
35 | });
36 |
37 | it(' renders correctly when no pagination set', () => {
38 | const Component = (
39 |
40 |
46 |
47 | );
48 |
49 | // Matches snapshot
50 | const tree = renderer.create(Component).toJSON();
51 | expect(tree).toMatchSnapshot();
52 | expect(tree).toEqual(null);
53 | });
54 |
55 | it(' renders correctly when loading', () => {
56 | const Component = (
57 |
58 |
72 |
73 | );
74 |
75 | // Matches snapshot
76 | const tree = renderer.create(Component).toJSON();
77 | expect(tree).toMatchSnapshot();
78 | expect(tree).toEqual(null);
79 | });
80 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/__snapshots__/Error.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly 1`] = `
4 |
7 |
10 |
13 | Hello
14 |
15 |
18 | World
19 |
20 |
21 |
22 | `;
23 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/__snapshots__/Footer.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly 1`] = `
4 |
7 |
10 |
13 |
16 |
19 | © AwesomeProject. All Rights Reserved.
20 |
21 |
22 |
23 |
24 |
25 | `;
26 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/__snapshots__/Header.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly 1`] = `
4 |
100 | `;
101 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/__snapshots__/Loading.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly 1`] = `
4 |
7 |
10 |
13 |
25 | Loading
26 |
27 |
28 |
29 |
30 | `;
31 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/__snapshots__/MobileTabBar.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly 1`] = `
4 |
102 | `;
103 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/__snapshots__/PageTitle.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly 1`] = `
4 |
7 |
10 |
13 | Hello World
14 |
15 |
16 |
17 | `;
18 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/components/UI/__snapshots__/TablePagination.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly 1`] = `
4 |
7 |
10 | Showing 6 of 12
11 |
12 |
16 |
42 |
43 |
44 | `;
45 |
46 | exports[` renders correctly when loading 1`] = `null`;
47 |
48 | exports[` renders correctly when no pagination set 1`] = `null`;
49 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/constants/config.test.js:
--------------------------------------------------------------------------------
1 | import config from '../../constants/config';
2 |
3 | it('Config: Environment to be set correctly', () => {
4 | expect(config.isDevEnv).toBe(true);
5 | });
6 |
--------------------------------------------------------------------------------
/OURS-WEB/src/tests/lib/jwt.test.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import jwt from 'jsonwebtoken';
3 | import { hasActiveAuthToken, hasAuthToken, refreshAuthToken } from '../../lib/jwt';
4 |
5 | const cookies = require('../../lib/cookies');
6 |
7 | // eslint-disable-next-line
8 | const expiredToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjM2ZmNlYjVlNDE2ZTU0ODIyN2Y5NWM1NjIwMThlMjk4ZTdhMjllNzYzNWRhNTUxOTE1MTNjODkwZjJkYWFmZTBlMTQ5NTYyZjgyNjRhNjY1In0.eyJhdWQiOiIxIiwianRpIjoiMzZmY2ViNWU0MTZlNTQ4MjI3Zjk1YzU2MjAxOGUyOThlN2EyOWU3NjM1ZGE1NTE5MTUxM2M4OTBmMmRhYWZlMGUxNDk1NjJmODI2NGE2NjUiLCJpYXQiOjE1Njk1NDMwMjksIm5iZiI6MTU2OTU0MzAyOSwiZXhwIjoxNTY5NTU3NDI4LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.fsakIWOkRbwZHImtYLXwqJN3dxOGjZG6hPJ4phPl5Sjom4bA2LqZR8u7ar7JyqZoAc8NEuko6NpTE7YGtgL_EZIhGCimR7OTy14T41XNMxGAJuyU2Jg-8sA_Pnah7ksquXuDdVVAO755DepX1XuCNhMPIcfgICkT2XzQX5vw4ZLdsq3oZeI_fxDiwdLaivNLIAx21QRUH_jP1MhsKVEPyG16fnWgKoCMh-n_FbnhUcHOrDt9ndJseN4SsM6g2MaNb9VmRShTjzMTOZhG8jFsXjdQjgQJJxXZEnqW60nPDSQQm9NkA24JuZ3uZaTNsdKKEzFXkd5gbfdCRygbM9q8-Ybb5bWIOmHA_S9nbV6H9YCi6boyikTRJH5eFCwPA8JAC3O9Brq7fyPUYY2DGGbBp4HNrj-aQpmZGgZfAPRTdmSRBhFBeMAzZCXzvsvfv5be4Ox3Nrk_bqrEoK7gQaUwOCnO15gQJYXPfIlyW4Y299KGNyU8C59DfjKCcZPyNVh84XOnGzxvd3c9t5ceP54KQPTIQzp7yXbRtvvFIrEvVb_g9Kfk6O3I7vVL7IsorBG9CX2b6MBX_d2UGcThCd8DEK-ZRU4lJIk3PgmwVxhFAmV6m6QSOXVHdmNHdTJ7wibi5GJMzHdZTKbX75xxPsAl7C9d-kUHgvXeQiOMqW4iK2I';
9 |
10 | const jwtSecret = '123Abc';
11 | const validToken = jwt.sign({}, jwtSecret, { expiresIn: 86400 });
12 |
13 | jest.mock('axios');
14 |
15 | cookies.getCookie = jest.fn();
16 | cookies.deleteCookie = jest.fn();
17 |
18 | it('jwt: hasActiveAuthToken returns correctly', () => {
19 | cookies.getCookie
20 | // A valid token should return true
21 | .mockReturnValueOnce(validToken)
22 | // An Expired token should return false
23 | .mockReturnValueOnce(expiredToken)
24 | // A non-token should return false
25 | .mockReturnValueOnce('abc')
26 | // No token at all should return false
27 | .mockReturnValueOnce(undefined);
28 |
29 | expect(hasActiveAuthToken()).toBeTruthy();
30 | expect(hasActiveAuthToken()).toBeFalsy();
31 | expect(hasActiveAuthToken()).toBeFalsy();
32 | expect(hasActiveAuthToken()).toBeFalsy();
33 | });
34 |
35 | it('jwt: hasAuthToken returns correctly', () => {
36 | cookies.getCookie
37 | // A valid token should return true
38 | .mockReturnValueOnce(validToken)
39 | // An Expired token should return true
40 | .mockReturnValueOnce(expiredToken)
41 | // A non-token should return false
42 | .mockReturnValueOnce('abc')
43 | // No token at all should return false
44 | .mockReturnValueOnce(undefined);
45 |
46 | expect(hasAuthToken()).toBeTruthy();
47 | expect(hasAuthToken()).toBeTruthy();
48 | expect(hasAuthToken()).toBeFalsy();
49 | expect(hasAuthToken()).toBeFalsy();
50 | });
51 |
52 | it('jwt: refreshAuthToken returns correctly', () => {
53 | // eslint-disable-next-line
54 | axios.post.mockResolvedValue({ data: { token: validToken } });
55 |
56 | cookies.getCookie
57 | // No token should return false
58 | .mockReturnValueOnce(undefined)
59 | // An Expired token should return a token
60 | .mockReturnValueOnce(expiredToken);
61 |
62 | refreshAuthToken().then((res) => {
63 | expect(res).toBeFalsy();
64 | });
65 |
66 | refreshAuthToken().then((token) => {
67 | expect(token).toBeTruthy();
68 |
69 | jwt.verify(token, jwtSecret, (err, decoded) => {
70 | expect(err).toBeFalsy();
71 | expect(decoded.exp).toBeTruthy();
72 | });
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/OURS/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | # All files
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = space
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
--------------------------------------------------------------------------------
/OURS/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'airbnb',
3 | parser: 'babel-eslint',
4 | plugins: [
5 | 'jest'
6 | ],
7 | parserOptions: {
8 | ecmaFeatures: {
9 | classes: true
10 | }
11 | },
12 | env: {
13 | 'jest/globals': true
14 | },
15 | rules: {
16 | 'max-len': [2, {'code': 110, 'tabWidth': 2, 'ignoreUrls': true}],
17 | 'react/jsx-filename-extension': ['error', { 'extensions': ['.js', '.jsx'] }],
18 | 'global-require': 'off',
19 | 'no-console': 'off',
20 | 'import/no-extraneous-dependencies': 'off',
21 | 'import/extensions': 'off',
22 | 'import/no-unresolved': 'off',
23 | 'jsx-a11y/anchor-is-valid': 'off',
24 | 'no-underscore-dangle': 'off',
25 | 'prefer-promise-reject-errors': 'off',
26 | 'no-nested-ternary': 'off',
27 | 'react/no-multi-comp': 'off',
28 | 'react/no-unescaped-entities': 'off',
29 | 'jsx-a11y/click-events-have-key-events': 'off',
30 | 'jsx-a11y/no-static-element-interactions': 'off',
31 | 'react/jsx-props-no-spreading': 'off',
32 | 'react/jsx-fragments': 'off',
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/OURS/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: mcnamee
2 |
--------------------------------------------------------------------------------
/OURS/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: macOS-latest
8 | strategy:
9 | matrix:
10 | node-version: [12.x]
11 |
12 | steps:
13 | - uses: actions/checkout@v1
14 | - name: Use Node.js ${{ matrix.node-version }}
15 | uses: actions/setup-node@v1
16 | with:
17 | node-version: ${{ matrix.node-version }}
18 | - name: Install
19 | run: |
20 | yarn
21 | - name: Lint
22 | run: |
23 | ./node_modules/.bin/eslint "src/**/*.js"
24 | - name: Jest Tests
25 | run: |
26 | yarn test
27 | env:
28 | CI: true
29 |
--------------------------------------------------------------------------------
/OURS/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | bracketSpacing: true,
3 | singleQuote: true,
4 | trailingComma: 'all',
5 | printWidth: 100,
6 | jsxBracketSameLine: false,
7 | };
8 |
--------------------------------------------------------------------------------
/OURS/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ### Help us reproduce - tell us about your environment
4 |
5 |
6 | 1.
7 |
8 | ### Steps to reproduce
9 |
10 | 1.
11 |
12 | ### Expected result
13 |
14 | 1.
15 |
16 | ### Actual result
17 |
18 | 1. [Screenshot, logs]
19 |
--------------------------------------------------------------------------------
/OURS/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Matt Mcnamee
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 |
--------------------------------------------------------------------------------
/OURS/documentation/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Love to hear any feedback or tips to improve - submit an issue or a fix via a pull request.
4 |
5 | Please ensure you're following the below rules before submitting a PR:
6 |
7 | ## Naming Conventions
8 |
9 | Please follow [Airbnb's Name Conventions](https://github.com/airbnb/javascript#naming-conventions) from the style guide.
10 |
11 | ## File Structure & Naming Conventions
12 |
13 | - __Structure__
14 | - Follow the existing file structure
15 | - __Files__
16 | - Should be `lowercase`, with words separated by hyphens (`-`) eg. `logo-cropped.jpg`
17 | - With the exception of Containers and Components, which should be `PascalCase` - eg. `RecipeView.js`
18 | - __Directories__
19 | - Folder names should be `lowercase,` with words separated by a hyphen (`-`) - eg. `/components/case-studies`
20 | - Folders and files can be named singlular or plural - do what sounds right
21 | - If there's more than a few files in a directory that are related, group them within their own directory
22 | - eg. if I have 2 components: `/components/RecipeListing.js` and `/components/RecipeView.js`, I may choose to create a new directory within components called `recipes` and put the 2 files within (removing `Recipes`). The result would be: `/components/recipes/Listing.js` and `/components/recipes/View.js`
23 |
24 | ## Linting
25 |
26 | Please ensure your code is passing through the linter.
27 |
28 | ## Tests
29 |
30 | Please include tests with your code and ensure your code is passing the existing tests.
31 |
--------------------------------------------------------------------------------
/OURS/documentation/file-structure.md:
--------------------------------------------------------------------------------
1 | ## File structure
2 |
3 | - `/public` contains static assets like the HTML page we're planning to deploy to, or images. You can delete any file in this folder apart from `index.html`.
4 | - `/src` contains our JS and CSS code. `index.js` is the entry-point for our file, and is mandatory.
5 | - `/components` - 'Dumb-components' / presentational. [Read More →](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
6 | - `/constants` - App-wide variables
7 | - `/containers` - 'Smart-components' that connect business logic to presentation [Read More →](https://redux.js.org/docs/basics/UsageWithReact.html#presentational-and-container-components)
8 | - `/images` - ...
9 | - `/lib` - Utils and custom libraries
10 | - `/models` - Rematch models combining actions, reducers and state. [Read More →](https://github.com/rematch/rematch#step-2-models)
11 | - `/routes`- wire up the router with any & all screens [Read More →](https://github.com/aksonov/react-native-router-flux)
12 | - `/store`- Redux Store - hooks up the stores and provides initial/template states [Read More →](https://redux.js.org/docs/basics/Store.html)
13 | - `/styles`- all the SCSS you could dream of
14 | - `/tests` contains all of our tests, where the test file matches the resptive file from `/src`
15 | - `index.js` - The starting place for our app
16 |
--------------------------------------------------------------------------------
/OURS/documentation/testing.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | ## Linting (with eslint)
4 |
5 | Want to check if your code is formatted consistently + pick up on any syntax errors:
6 |
7 | ```
8 | ./node_modules/.bin/eslint "src/**/*.js"
9 | ```
10 |
11 | ## Writing and Running Tests
12 |
13 | This project is set up to use [jest](https://facebook.github.io/jest/) for tests. You can configure whatever testing strategy you like, but jest works out of the box. Create test files within the directory (from root) called `/__tests__/` to have them loaded by jest. See the [the template project](https://github.com/react-community/create-react-native-app/blob/master/react-native-scripts/template/App.test.js) for an example test. The [jest documentation](https://facebook.github.io/jest/docs/en/getting-started.html) is also a wonderful resource, as is the [React Native testing tutorial](https://facebook.github.io/jest/docs/en/tutorial-react-native.html).
14 |
15 | #### `yarn test`
16 |
17 | Runs the [jest](https://github.com/facebook/jest) test runner on your tests.
18 |
19 | ## Jest Snapshots
20 |
21 | Run `yarn test` to run a test add `-- --watch` to run it in developer mode.
22 |
23 | To run an individual Jest test:
24 | * Run `jest path/to/test.js` if you have Jest installed globally
25 | * Run `node_modules/.bin/jest path/to/test.js` to use the projects Jest installation
26 |
27 | Tests should be placed in the root `__tests__` directory, followed by their related parents folder to keep consistency, i.e `/__tests__/containers/ExampleForm.js`
28 |
29 | - (Snapshot testing) https://facebook.github.io/jest/docs/tutorial-react-native.html#snapshot-test
30 | - (DOM testing WIP) https://facebook.github.io/jest/docs/tutorial-react.html#dom-testing
31 |
--------------------------------------------------------------------------------
/OURS/src/constants/messages.js:
--------------------------------------------------------------------------------
1 | export const generalMessages = {};
2 |
3 | export const successMessages = {
4 | // Defaults
5 | defaultForm: 'Success - Form Saved',
6 |
7 | // Member
8 | login: 'You are now logged in',
9 | signUp: 'You are now signed up. Please login to continue.',
10 | forgotPassword: 'Password reset. Please check your email.',
11 | };
12 |
13 | export const errorMessages = {
14 | // Defaults
15 | default: 'Hmm, an unknown error occured',
16 | timeout: 'Server Timed Out. Check your internet connection',
17 | invalidJson: 'Response returned is not valid JSON',
18 | missingData: 'Missing data',
19 |
20 | // Member
21 | memberNotAuthd: 'You need to be logged in, to update your profile',
22 | memberExists: 'Member already exists',
23 | missingFirstName: 'First name is missing',
24 | missingLastName: 'Last name is missing',
25 | missingEmail: 'Email is missing',
26 | missingPassword: 'Password is missing',
27 | passwordsDontMatch: 'Passwords do not match',
28 |
29 | // Articles
30 | articlesEmpty: 'No articles found',
31 | articles404: 'This article could not be found',
32 | };
33 |
--------------------------------------------------------------------------------
/OURS/src/containers/Articles/Form.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import Layout from '../../components/Articles/Form';
5 |
6 | class ArticlesFormContainer extends Component {
7 | constructor() {
8 | super();
9 | this.state = { error: null, success: null, loading: false };
10 | }
11 |
12 | /**
13 | * On Form Submission
14 | */
15 | onFormSubmit = async (data) => {
16 | const { onFormSubmit } = this.props;
17 |
18 | this.setState({ success: null, error: null, loading: true });
19 |
20 | try {
21 | const success = await onFormSubmit(data);
22 | this.setState({ success, error: null, loading: false });
23 | } catch (error) {
24 | this.setState({ loading: false, success: null, error: error.message });
25 | }
26 | }
27 |
28 | /**
29 | * Render
30 | */
31 | render = () => {
32 | const { userInput } = this.props;
33 | const { error, loading, success } = this.state;
34 |
35 | return (
36 |
43 | );
44 | }
45 | }
46 |
47 | ArticlesFormContainer.propTypes = {
48 | userInput: PropTypes.shape({}).isRequired,
49 | onFormSubmit: PropTypes.func.isRequired,
50 | };
51 |
52 | const mapStateToProps = (state) => ({
53 | userInput: state.articles.userInput || {},
54 | });
55 |
56 | const mapDispatchToProps = (dispatch) => ({
57 | onFormSubmit: dispatch.articles.save,
58 | });
59 |
60 | export default connect(mapStateToProps, mapDispatchToProps)(ArticlesFormContainer);
61 |
--------------------------------------------------------------------------------
/OURS/src/containers/Articles/List.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import Layout from '../../components/Articles/List';
5 |
6 | class ArticlesListContainer extends Component {
7 | constructor(props) {
8 | super();
9 |
10 | // Prioritize (web) page route over last meta value
11 | const page = props.page || props.meta.page;
12 |
13 | this.state = {
14 | error: null, loading: false, page: parseInt(page, 10) || 1,
15 | };
16 | }
17 |
18 | componentDidMount = () => this.fetchData();
19 |
20 | /**
21 | * If the page prop changes, update state
22 | */
23 | componentDidUpdate = (prevProps) => {
24 | const { page } = this.props;
25 | const { page: prevPage } = prevProps;
26 |
27 | if (page !== prevPage) {
28 | // eslint-disable-next-line
29 | this.setState({
30 | error: null, loading: false, page: parseInt(page, 10) || 1,
31 | }, this.fetchData);
32 | }
33 | }
34 |
35 | /**
36 | * Fetch Data
37 | */
38 | fetchData = async ({ forceSync = false, incrementPage = false } = {}) => {
39 | const { fetchData } = this.props;
40 |
41 | let { page } = this.state;
42 | page = incrementPage ? page + 1 : page; // Force fetch the next page worth of data when requested
43 | page = forceSync ? 1 : page; // Start from scratch
44 |
45 | this.setState({ loading: true, error: null, page });
46 |
47 | try {
48 | await fetchData({ forceSync, page });
49 | this.setState({ loading: false, error: null });
50 | } catch (err) {
51 | this.setState({ loading: false, error: err.message });
52 | }
53 | };
54 |
55 | /**
56 | * Render
57 | */
58 | render = () => {
59 | const {
60 | listFlat, listPaginated, pagination, meta,
61 | } = this.props;
62 | const { loading, error, page } = this.state;
63 |
64 | return (
65 |
75 | );
76 | };
77 | }
78 |
79 | ArticlesListContainer.propTypes = {
80 | listFlat: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
81 | listPaginated: PropTypes.shape({}).isRequired,
82 | meta: PropTypes.shape({
83 | page: PropTypes.number,
84 | }).isRequired,
85 | fetchData: PropTypes.func.isRequired,
86 | pagination: PropTypes.arrayOf(PropTypes.shape()).isRequired,
87 | page: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
88 | };
89 |
90 | ArticlesListContainer.defaultProps = {
91 | page: 1,
92 | };
93 |
94 | const mapStateToProps = (state) => ({
95 | listFlat: state.articles.listFlat || [],
96 | listPaginated: state.articles.listPaginated || {},
97 | meta: state.articles.meta || [],
98 | pagination: state.articles.pagination || {},
99 | });
100 |
101 | const mapDispatchToProps = (dispatch) => ({
102 | fetchData: dispatch.articles.fetchList,
103 | });
104 |
105 | export default connect(mapStateToProps, mapDispatchToProps)(ArticlesListContainer);
106 |
--------------------------------------------------------------------------------
/OURS/src/containers/Articles/Single.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import Layout from '../../components/Articles/Single';
5 |
6 | class ArticlesSingleContainer extends Component {
7 | constructor() {
8 | super();
9 | this.state = { loading: false, error: null, article: {} };
10 | }
11 |
12 | componentDidMount = () => this.fetchData();
13 |
14 | /**
15 | * Fetch Data
16 | */
17 | fetchData = async () => {
18 | const { fetchData, id } = this.props;
19 |
20 | this.setState({ loading: true, error: null });
21 |
22 | try {
23 | const article = await fetchData(id);
24 | this.setState({ loading: false, error: null, article });
25 | } catch (err) {
26 | this.setState({ loading: false, error: err.message, article: {} });
27 | }
28 | };
29 |
30 | /**
31 | * Render
32 | */
33 | render = () => {
34 | const { loading, error, article } = this.state;
35 |
36 | return ;
37 | };
38 | }
39 |
40 | ArticlesSingleContainer.propTypes = {
41 | fetchData: PropTypes.func.isRequired,
42 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
43 | };
44 |
45 | ArticlesSingleContainer.defaultProps = {
46 | id: null,
47 | };
48 |
49 | const mapStateToProps = () => ({});
50 |
51 | const mapDispatchToProps = (dispatch) => ({
52 | fetchData: dispatch.articles.fetchSingle,
53 | });
54 |
55 | export default connect(mapStateToProps, mapDispatchToProps)(ArticlesSingleContainer);
56 |
--------------------------------------------------------------------------------
/OURS/src/containers/index.js:
--------------------------------------------------------------------------------
1 | // Articles
2 | export { default as ArticlesForm } from './Articles/Form';
3 | export { default as ArticlesList } from './Articles/List';
4 | export { default as ArticlesSingle } from './Articles/Single';
5 |
--------------------------------------------------------------------------------
/OURS/src/images/app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS/src/images/app-icon.png
--------------------------------------------------------------------------------
/OURS/src/images/launch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcnamee/react-native-boilerplate-builder/28076ec2c3426a6b65744ff7a843544153c40049/OURS/src/images/launch.png
--------------------------------------------------------------------------------
/OURS/src/lib/format-error-messages.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Create a readable error message for the front-end user
3 | */
4 | export default (error) => {
5 | /*
6 | For an error response like:
7 | {
8 | "message": "422 Unprocessable Entity",
9 | "errors": {
10 | "email": [
11 | "The email must be a valid email address."
12 | ]
13 | }
14 | }
15 | */
16 | if (error && error.errors) {
17 | let errors = '';
18 | Object.entries(error.errors).forEach((v) => {
19 | errors += v[1].join(', ');
20 | });
21 | return Error(errors);
22 | }
23 |
24 | /*
25 | For an error response like:
26 | {
27 | "error": {
28 | "message": "403 Forbidden",
29 | "status_code": 403
30 | }
31 | }
32 | */
33 | if (error && error.message) {
34 | return Error(error.message);
35 | }
36 |
37 | // When an Error - return the error
38 | if (error instanceof Error) {
39 | return error;
40 | }
41 |
42 | // Otherwise create an error
43 | return new Error('Uh oh - something happened');
44 | };
45 |
--------------------------------------------------------------------------------
/OURS/src/lib/images.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Sort through the mass amounts of data in
3 | * an endpoint and return the featured image URL
4 | */
5 | // eslint-disable-next-line
6 | export const getFeaturedImageUrl = (item) => (
7 | (item._embedded
8 | && item._embedded['wp:featuredmedia']
9 | && item._embedded['wp:featuredmedia']['0']
10 | && item._embedded['wp:featuredmedia']['0'].media_details
11 | && item._embedded['wp:featuredmedia']['0'].media_details.sizes
12 | && item._embedded['wp:featuredmedia']['0'].media_details.sizes.full
13 | && item._embedded['wp:featuredmedia']['0'].media_details.sizes.full.source_url)
14 | || null
15 | );
16 |
--------------------------------------------------------------------------------
/OURS/src/lib/pagination.js:
--------------------------------------------------------------------------------
1 | export default (lastPage, link) => {
2 | const pagination = [];
3 | const upTo = parseInt(lastPage, 10);
4 |
5 | if (upTo > 1) {
6 | for (let p = 1; p <= upTo; p++) { // eslint-disable-line
7 | if (p === 1) {
8 | pagination.push({ title: p, link });
9 | } else {
10 | pagination.push({ title: p, link: `${link}${p}` });
11 | }
12 | }
13 | }
14 |
15 | return pagination;
16 | };
17 |
--------------------------------------------------------------------------------
/OURS/src/lib/string.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Uppercase the first letter in a string
3 | * @param {str} str
4 | * @return {str}
5 | */
6 | export const ucfirst = (string) => {
7 | if (!string) {
8 | return '';
9 | }
10 |
11 | return string.charAt(0).toUpperCase() + string.slice(1);
12 | };
13 |
14 | /**
15 | * Uppercase the first letter in a string
16 | * @param {str} input content
17 | * @param {num} number of words to cut off at
18 | * @return {str}
19 | */
20 | export const truncate = (inputContent, numWords) => {
21 | if (!inputContent) {
22 | return '';
23 | }
24 | const limit = !numWords ? 100 : numWords;
25 |
26 | // Trim whitespace
27 | let content = inputContent.trim();
28 |
29 | // Convert the content into an array of words
30 | const contentArr = content.split(' ');
31 |
32 | // Remove any words above the limit
33 | content = contentArr.slice(0, limit);
34 |
35 | // Convert the array of words back into a string
36 | return `${content.join(' ')}${contentArr.length > limit ? '…' : ''}`;
37 | };
38 |
39 | /**
40 | * Strip any HTML from a string
41 | * @param {str} string
42 | */
43 | export const stripHtml = (string) => {
44 | let returnString = string;
45 |
46 | // Remove DOM tags
47 | returnString = returnString.replace(/<[^>]*>?/gm, '');
48 |
49 | // Remove entities
50 | const entities = [
51 | ['amp', '&'],
52 | ['apos', "'"],
53 | ['#x27', "'"],
54 | ['#x2F', '/'],
55 | ['#39', "'"],
56 | ['#47', '/'],
57 | ['lt', '<'],
58 | ['gt', '>'],
59 | ['nbsp', ' '],
60 | ['quot', '"'],
61 | ['hellip', '...'],
62 | ['#8217', "'"],
63 | ['#8230', '...'],
64 | ['#8211', '-'],
65 | ];
66 |
67 | entities.map((item) => { // eslint-disable-line
68 | returnString = returnString.replace(new RegExp(`&${item[0]};`, 'g'), item[1]);
69 | });
70 |
71 | return returnString;
72 | };
73 |
--------------------------------------------------------------------------------
/OURS/src/models/articles.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import Api from '../lib/api';
3 | import HandleErrorMessage from '../lib/format-error-messages';
4 | import initialState from '../store/articles';
5 | import Config from '../constants/config';
6 | import { getFeaturedImageUrl } from '../lib/images';
7 | import { ucfirst, stripHtml } from '../lib/string';
8 | import { errorMessages, successMessages } from '../constants/messages';
9 | import pagination from '../lib/pagination';
10 |
11 | /**
12 | * Transform the endpoint data structure into our redux store format
13 | * @param {obj} item
14 | */
15 | const transform = (item) => ({
16 | id: item.id || 0,
17 | name: item.title && item.title.rendered ? ucfirst(stripHtml(item.title.rendered)) : '',
18 | content: item.content && item.content.rendered ? stripHtml(item.content.rendered) : '',
19 | contentRaw: item.content && item.content.rendered,
20 | excerpt: item.excerpt && item.excerpt.rendered ? stripHtml(item.excerpt.rendered) : '',
21 | date: moment(item.date).format(Config.dateFormat) || '',
22 | slug: item.slug || null,
23 | link: item.link || null,
24 | image: getFeaturedImageUrl(item),
25 | });
26 |
27 | export default {
28 | namespace: 'articles',
29 |
30 | /**
31 | * Initial state
32 | */
33 | state: initialState,
34 |
35 | /**
36 | * Effects/Actions
37 | */
38 | effects: (dispatch) => ({
39 | /**
40 | * Get a list from the API
41 | * @param {obj} rootState
42 | * @returns {Promise}
43 | */
44 | async fetchList({ forceSync = false, page = 1 } = {}, rootState) {
45 | const { articles = {} } = rootState;
46 | const { lastSync = {}, meta = {} } = articles;
47 | const { lastPage } = meta;
48 |
49 | // Only sync when it's been 5mins since last sync
50 | if (lastSync[page]) {
51 | if (!forceSync && moment().isBefore(moment(lastSync[page]).add(5, 'minutes'))) {
52 | return true;
53 | }
54 | }
55 |
56 | // We've reached the end of the list
57 | if (page && lastPage && page > lastPage) {
58 | throw HandleErrorMessage({ message: `Page ${page} does not exist` });
59 | }
60 |
61 | try {
62 | const response = await Api.get(`/v2/posts?per_page=4&page=${page}&orderby=modified&_embed`);
63 | const { data, headers } = response;
64 |
65 | return !data || data.length < 1
66 | ? true
67 | : dispatch.articles.replace({ data, headers, page });
68 | } catch (error) {
69 | throw HandleErrorMessage(error);
70 | }
71 | },
72 |
73 | /**
74 | * Get a single item from the API
75 | * @param {number} id
76 | * @returns {Promise[obj]}
77 | */
78 | async fetchSingle(id) {
79 | try {
80 | const response = await Api.get(`/v2/posts/${id}?_embed`);
81 | const { data } = response;
82 |
83 | if (!data) {
84 | throw new Error({ message: errorMessages.articles404 });
85 | }
86 |
87 | return transform(data);
88 | } catch (error) {
89 | throw HandleErrorMessage(error);
90 | }
91 | },
92 |
93 | /**
94 | * Save date to redux store
95 | * @param {obj} data
96 | * @returns {Promise[obj]}
97 | */
98 | async save(data) {
99 | try {
100 | if (Object.keys(data).length < 1) {
101 | throw new Error({ message: errorMessages.missingData });
102 | }
103 |
104 | dispatch.articles.replaceUserInput(data);
105 | return successMessages.defaultForm; // Message for the UI
106 | } catch (error) {
107 | throw HandleErrorMessage(error);
108 | }
109 | },
110 | }),
111 |
112 | /**
113 | * Reducers
114 | */
115 | reducers: {
116 | /**
117 | * Replace list in store
118 | * @param {obj} state
119 | * @param {obj} payload
120 | */
121 | replace(state, payload) {
122 | let newList = null;
123 | const { data, headers, page } = payload;
124 |
125 | // Loop data array, saving items in a usable format
126 | if (data && typeof data === 'object') {
127 | newList = data.map((item) => transform(item));
128 | }
129 |
130 | // Create our paginated and flat lists
131 | const listPaginated = page === 1 ? { [page]: newList } : { ...state.listPaginated, [page]: newList };
132 | const listFlat = Object.keys(listPaginated).map((k) => listPaginated[k]).flat() || [];
133 |
134 | return newList
135 | ? {
136 | ...state,
137 | listPaginated,
138 | listFlat,
139 | lastSync: page === 1
140 | ? { [page]: moment().format() }
141 | : { ...state.lastSync, [page]: moment().format() },
142 | meta: {
143 | page,
144 | lastPage: parseInt(headers['x-wp-totalpages'], 10) || null,
145 | total: parseInt(headers['x-wp-total'], 10) || null,
146 | },
147 | pagination: pagination(headers['x-wp-totalpages'], '/articles/'),
148 | }
149 | : initialState;
150 | },
151 |
152 | /**
153 | * Save form data
154 | * @param {obj} state
155 | * @param {obj} payload
156 | */
157 | replaceUserInput(state, payload) {
158 | return {
159 | ...state,
160 | userInput: payload,
161 | };
162 | },
163 | },
164 | };
165 |
--------------------------------------------------------------------------------
/OURS/src/models/index.js:
--------------------------------------------------------------------------------
1 | export { default as articles } from './articles'; // eslint-disable-line
2 |
--------------------------------------------------------------------------------
/OURS/src/store/articles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | listPaginated: {
3 | 1: [{
4 | placeholder: true,
5 | id: 0,
6 | name: '---- --- -- ------',
7 | content: '---- --- -- ------ ---- --- -- ------ ---- --- -- ------ ---- --- -- ------',
8 | excerpt: '---- --- -- ------ ---- --- -- ------ ---- --- -- ------ ---- --- -- ------',
9 | image: 'https://www.digitalsupply.co/wp-content/uploads/2018/03/glacier-blue.jpg',
10 | date: '-- / -- / ----',
11 | slug: '-----',
12 | link: '----.---.--/------',
13 | }],
14 | },
15 | listFlat: [{
16 | placeholder: true,
17 | id: 0,
18 | name: '---- --- -- ------',
19 | content: '---- --- -- ------ ---- --- -- ------ ---- --- -- ------ ---- --- -- ------',
20 | excerpt: '---- --- -- ------ ---- --- -- ------ ---- --- -- ------ ---- --- -- ------',
21 | image: 'https://www.digitalsupply.co/wp-content/uploads/2018/03/glacier-blue.jpg',
22 | date: '-- / -- / ----',
23 | slug: '-----',
24 | link: '----.---.--/------',
25 | }],
26 | meta: {
27 | page: 1,
28 | lastPage: null,
29 | total: null,
30 | },
31 | lastSync: {},
32 | pagination: [],
33 | userInput: { email: '' },
34 | };
35 |
--------------------------------------------------------------------------------
/OURS/src/tests/__mocks__/react-native-gesture-handler.js:
--------------------------------------------------------------------------------
1 | export default {};
2 |
--------------------------------------------------------------------------------
/OURS/src/tests/__mocks__/react-native-reanimated.js:
--------------------------------------------------------------------------------
1 | export default {};
2 |
--------------------------------------------------------------------------------
/OURS/src/tests/__mocks__/react-native-tab-view.js:
--------------------------------------------------------------------------------
1 | export default {};
2 |
--------------------------------------------------------------------------------
/OURS/src/tests/__mocks__/react-navigation-stack.js:
--------------------------------------------------------------------------------
1 | export default {};
2 |
--------------------------------------------------------------------------------
/OURS/src/tests/__mocks__/react-redux.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This mock will make sure that we are able to access mapStateToProps,
3 | * mapDispatchToProps and reactComponent in the test file.
4 | */
5 |
6 | // To use this, just do `jest.mock('react-redux');` in your test.js file.
7 | const mock = jest.fn((action) => action);
8 |
9 | module.exports = {
10 | connect: (mapStateToProps, mapDispatchToProps) => (reactComponent) => ({
11 | mapStateToProps,
12 | mapDispatchToProps: (dispatch = mock, ownProps) => mapDispatchToProps(dispatch, ownProps),
13 | reactComponent,
14 | mock,
15 | }),
16 | Provider: ({ children }) => children,
17 | };
18 |
--------------------------------------------------------------------------------
/OURS/src/tests/lib/format-error-messages.test.js:
--------------------------------------------------------------------------------
1 | import formatErrors from '../../lib/format-error-messages';
2 |
3 | it('Errors to be in a consistent format', () => {
4 | // Object passed
5 | expect(formatErrors({ message: 'Hi!' })).toStrictEqual(new Error('Hi!'));
6 |
7 | // Error passed
8 | expect(formatErrors(new Error('Hi!'))).toStrictEqual(new Error('Hi!'));
9 |
10 | // Laravel Error Object passed
11 | const validationError = {
12 | message: '422 Unprocessable Entity',
13 | errors: {
14 | firstName: ['The first name must be a valid name.'],
15 | email: ['The email must be a valid email address.'],
16 | },
17 | };
18 | expect(formatErrors(validationError))
19 | .toStrictEqual(new Error('The first name must be a valid name.The email must be a valid email address.'));
20 | });
21 |
--------------------------------------------------------------------------------
/OURS/src/tests/lib/images.test.js:
--------------------------------------------------------------------------------
1 | import { getFeaturedImageUrl } from '../../lib/images';
2 |
3 | it('lib/string: getFeaturedImageUrl returns correctly', () => {
4 | expect(getFeaturedImageUrl({
5 | _embedded: {
6 | 'wp:featuredmedia': [{
7 | media_details: {
8 | sizes: {
9 | full: {
10 | source_url: 'https://www.digitalsupply.co/wp-content/uploads/2018/03/glacier-blue.jpg',
11 | },
12 | },
13 | },
14 | }],
15 | },
16 | })).toEqual('https://www.digitalsupply.co/wp-content/uploads/2018/03/glacier-blue.jpg');
17 | });
18 |
--------------------------------------------------------------------------------
/OURS/src/tests/lib/pagination.test.js:
--------------------------------------------------------------------------------
1 | import pagination from '../../lib/pagination';
2 |
3 | it('lib/pagination: pagination returns correctly', () => {
4 | const threePages = pagination(3, '/articles/');
5 | expect(threePages).toEqual([
6 | { title: 1, link: '/articles/' },
7 | { title: 2, link: '/articles/2' },
8 | { title: 3, link: '/articles/3' },
9 | ]);
10 |
11 | // No links when only 1 page
12 | const onePage = pagination({ last_page: 1 }, '/articles/');
13 | expect(onePage).toEqual([]);
14 | });
15 |
--------------------------------------------------------------------------------
/OURS/src/tests/lib/string.test.js:
--------------------------------------------------------------------------------
1 | import { ucfirst, truncate } from '../../lib/string';
2 |
3 | it('lib/string: ucfirst returns correctly', () => {
4 | const lcWord = ucfirst('hello');
5 | expect(lcWord).toEqual('Hello');
6 |
7 | const upWord = ucfirst('Hello');
8 | expect(upWord).toEqual('Hello');
9 |
10 | const lcWords = ucfirst('hello world');
11 | expect(lcWords).toEqual('Hello world');
12 |
13 | const upWords = ucfirst('Hello world');
14 | expect(upWords).toEqual('Hello world');
15 |
16 | const numbers = ucfirst('1234 world');
17 | expect(numbers).toEqual('1234 world');
18 | });
19 |
20 | it('lib/string: truncate returns correctly', () => {
21 | const lcWord = truncate('hello world this is', 2);
22 | expect(lcWord).toEqual('hello world…');
23 |
24 | const upWord = truncate('Hello', 3);
25 | expect(upWord).toEqual('Hello');
26 |
27 | const lcWords = truncate('hello world world this is a big');
28 | expect(lcWords).toEqual('hello world world this is a big');
29 | });
30 |
--------------------------------------------------------------------------------
/OURS/src/tests/models/articles.test.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import Api from '../../lib/api';
3 | import model from '../../models/articles';
4 | import { successMessages } from '../../constants/messages';
5 |
6 | /**
7 | * Mocks
8 | */
9 | jest.mock('axios');
10 | afterEach(jest.resetAllMocks);
11 |
12 | const mockInput = {
13 | title: { rendered: 'hello world' },
14 | content: { rendered: 'Hello there fellows
' },
15 | excerpt: { rendered: 'Hello there fellows
' },
16 | _embedded: {
17 | 'wp:featuredmedia': [{
18 | media_details: {
19 | sizes: {
20 | full: {
21 | source_url: 'https://www.digitalsupply.co/wp-content/uploads/2018/03/glacier-blue.jpg',
22 | },
23 | },
24 | },
25 | }],
26 | },
27 | date: '2017-04-14T15:32:29',
28 | slug: 'using-open-source-software-to-build-your-instagram-followers',
29 | link: 'https://www.digitalsupply.co/using-open-source-software-to-build-your-instagram-followers/',
30 | };
31 |
32 | const mockOutput = {
33 | id: 0,
34 | name: 'Hello world',
35 | content: 'Hello there fellows',
36 | contentRaw: 'Hello there fellows
',
37 | excerpt: 'Hello there fellows',
38 | image: 'https://www.digitalsupply.co/wp-content/uploads/2018/03/glacier-blue.jpg',
39 | date: '14th Apr 2017',
40 | slug: 'using-open-source-software-to-build-your-instagram-followers',
41 | link: 'https://www.digitalsupply.co/using-open-source-software-to-build-your-instagram-followers/',
42 | };
43 |
44 | /**
45 | * Tests
46 | */
47 | it('Articles fetchList() returns correctly', async () => {
48 | Api.get.mockResolvedValue({ data: [mockInput], headers: { 'x-wp-totalpages': 3 } });
49 | const initialState = { articles: { lastSync: {} } };
50 | const dispatch = { articles: { replace: jest.fn((res) => res) } };
51 |
52 | await model.effects(dispatch).fetchList({ page: 2 }, initialState).then((res) => {
53 | expect(Api.get).toHaveBeenCalledWith('/v2/posts?per_page=4&page=2&orderby=modified&_embed');
54 | expect(dispatch.articles.replace).toHaveBeenCalledTimes(1);
55 | expect(res).toEqual({
56 | data: [mockInput],
57 | headers: { 'x-wp-totalpages': 3 },
58 | page: 2,
59 | });
60 | });
61 | });
62 |
63 | it('Articles fetchList() does not go to API if lastSync just set', async () => {
64 | const initialState = { articles: { lastSync: { 2: moment() } } };
65 | await model.effects().fetchList({ page: 2 }, initialState).then((res) => expect(res).toEqual(true));
66 | });
67 |
68 | it('Articles fetchSingle() returns correctly', async () => {
69 | Api.get.mockResolvedValue({ data: mockInput });
70 |
71 | await model.effects().fetchSingle(222).then((res) => {
72 | expect(Api.get).toHaveBeenCalledWith('/v2/posts/222?_embed');
73 | expect(res).toEqual(mockOutput);
74 | });
75 | });
76 |
77 | it('Articles Model returns correctly', () => {
78 | expect(model.reducers.replace({}, {
79 | page: 1,
80 | headers: {
81 | 'x-wp-totalpages': 2,
82 | 'x-wp-total': 2,
83 | },
84 | data: [mockInput],
85 | })).toMatchObject({
86 | meta: {
87 | lastPage: 2,
88 | total: 2,
89 | },
90 | pagination: [
91 | { title: 1, link: '/articles/' },
92 | { title: 2, link: '/articles/2' },
93 | ],
94 | listPaginated: {
95 | 1: [mockOutput],
96 | },
97 | });
98 | });
99 |
100 | it('Articles save() returns correctly', () => {
101 | const dispatch = { articles: { replaceUserInput: jest.fn((res) => res) } };
102 |
103 | model.effects(dispatch).save('hello@hello.com').then((res) => {
104 | expect(res).toEqual(successMessages.defaultForm);
105 | });
106 | });
107 |
108 | it('Articles Model Save returns correctly', () => {
109 | expect(model.reducers.replaceUserInput({
110 | meta: {
111 | lastPage: 2,
112 | total: 2,
113 | },
114 | pagination: [
115 | { title: 1, link: '/articles/' },
116 | { title: 2, link: '/articles/2' },
117 | ],
118 | listPaginated: {
119 | 1: [mockOutput],
120 | },
121 | }, 'hello@hello.com')).toMatchObject({
122 | meta: {
123 | lastPage: 2,
124 | total: 2,
125 | },
126 | pagination: [
127 | { title: 1, link: '/articles/' },
128 | { title: 2, link: '/articles/2' },
129 | ],
130 | listPaginated: {
131 | 1: [mockOutput],
132 | },
133 | userInput: 'hello@hello.com',
134 | });
135 | });
136 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Boilerplate Builder
2 |
3 | This repo is used to create a new React &/or React Native App, using the latest version of React/React Native and all dependencies.
4 |
5 | All new commits (changes to this repo) are automatically pushed to:
6 |
7 | - [React Native Starter Kit](https://github.com/mcnamee/react-native-starter-kit)
8 | - [React Native Starter Kit (Expo version)](https://github.com/mcnamee/react-native-expo-starter-kit)
9 | - [React Starter Kit (web)](https://github.com/mcnamee/react-starter-kit)
10 |
11 | `build.sh` essentially just:
12 |
13 | - `npx create-react-app`, `react-native init` or `expo init`'s a new app
14 | - Adds a bunch of commonly used dependencies (eg. Redux, a Router, Forms etc)
15 | - Adds familiar developer dependencies like the AirBnB linting code style
16 | - Adds a simple boilerplate codebase (with things like a directory structure, Redux and the Router configured, common components etc)
17 | - Adds familiar IDE configuration like prettier and eslint
18 | - Documentation for common tasks
19 | - (React Native) Fastlane configuration for App Store deployment
20 |
21 | ### ❓ Why?
22 |
23 | I was used to using a boilerplate app when building a new app, where I'd spend the first few hours updating dependencies and diffing against the latest version of a fresh React/React Native app. I wanted each project to use the latest and greatest (#fomo).
24 |
25 | ### ❓ Why Not?
26 |
27 | Creating a project where dependency versions are not locked, can lead to instability. For example if dependency-X's latest version is a major release ahead of the last tested version, it may break your new app. Be aware.
28 |
29 | ## 🔨 Requirements
30 |
31 | - MacOS _(this creation script has only been tested on a Mac)_
32 | - Node v15+
33 | - NPM v6+
34 | - `yarn`
35 | - `rsync`
36 | - Cocoapods (for React Native)
37 |
38 | ## 🚀 Usage
39 |
40 | ```bash
41 | bash build.sh
42 | ```
43 |
--------------------------------------------------------------------------------