├── .editorconfig ├── .env.yml ├── .expo-shared └── assets.json ├── .gitignore ├── App.tsx ├── README.md ├── app.json ├── babel.config.js ├── now.json ├── package.json ├── patches ├── @types+lunr+2.3.2.patch └── react-native-elements+1.1.0.patch ├── server.js ├── src ├── @types │ ├── ArgumentTypes.d.ts │ ├── Omit.d.ts │ ├── RecursivePartial.d.ts │ ├── graphql-tag.d.ts │ ├── graphql-tags.d.ts │ ├── isbot │ │ └── index.d.ts │ └── media.d.ts ├── GlobalState.tsx ├── assets │ └── img │ │ ├── brian-eus-author.jpg │ │ ├── logo-icon.png │ │ ├── logo-icon.svg │ │ ├── logo-square.png │ │ ├── preview.png │ │ ├── space.jpg │ │ └── splash.png ├── components │ ├── elements │ │ ├── Avatar.tsx │ │ └── index.tsx │ ├── lib │ │ ├── Helmet.tsx │ │ ├── Helmet.web.tsx │ │ ├── HoverObserver.tsx │ │ ├── HoverObserver.web.tsx │ │ ├── Routing.tsx │ │ ├── Routing.web.tsx │ │ ├── Touchables.tsx │ │ └── Touchables.web.tsx │ ├── modules │ │ ├── Logo.module.tsx │ │ └── Sidebar.module.tsx │ ├── screens │ │ ├── AppLoading.screen.tsx │ │ ├── AppLoading.screen.web.tsx │ │ ├── Home.screen.tsx │ │ ├── InnerPage.screen.tsx │ │ ├── Login.screen.tsx │ │ ├── Menu.screen.tsx │ │ ├── NotFound.screen.tsx │ │ ├── Notifications.screen.tsx │ │ ├── Post.screen.tsx │ │ ├── Register.screen.tsx │ │ ├── Search.screen.tsx │ │ ├── Settings.screen.tsx │ │ └── UserEdit.screen.tsx │ ├── sections │ │ ├── FooterEnd.section.tsx │ │ ├── FooterFixed.section.tsx │ │ ├── HeaderDefault.section.tsx │ │ ├── HeaderInnerPage.section.tsx │ │ └── Sidebar.section.tsx │ └── svgs │ │ └── LogoIcon.tsx ├── config │ ├── App.config.tsx │ ├── Routes.config.tsx │ └── Theme.config.tsx ├── lib │ ├── AssetLoading.tsx │ ├── AssetsLoading.web.tsx │ ├── Cloudinary.tsx │ ├── MobxPersistClass.tsx │ ├── MobxPersistObject.tsx │ ├── OfflineStorage.tsx │ ├── OfflineStorage.web.tsx │ ├── Polyfills.tsx │ └── getViewportInfo.tsx ├── mockApi │ ├── MockOrm.tsx │ ├── dbseed.tsx │ ├── hooks │ │ ├── errorHelpers.tsx │ │ ├── useMutation.tsx │ │ └── useQuery.tsx │ ├── index.tsx.bak │ └── models │ │ ├── BaseModel.tsx │ │ ├── notifications │ │ ├── db.json │ │ ├── dbseed.tsx │ │ └── index.tsx │ │ ├── posts │ │ ├── db.json │ │ ├── dbseed.tsx │ │ └── index.tsx │ │ └── users │ │ ├── db.json │ │ ├── dbseed.tsx │ │ └── index.tsx ├── model │ ├── SanitizerBase.tsx │ ├── notifications │ │ ├── santizer.tsx │ │ └── type.tsx │ ├── posts │ │ ├── sanitizer.tsx │ │ └── type.tsx │ └── users │ │ ├── sanitizer.tsx │ │ └── type.tsx └── serviceWorker.tsx ├── tsconfig.json ├── tslint.json ├── utils ├── create-indexes.js ├── tsxify-svg.sh └── tsxify-svg.tslint.json ├── web ├── favicon.ico ├── index.html ├── preview.png └── sitemap.xml ├── webpack.config.js └── yarn.lock /.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 | tab_width = 2 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.env.yml: -------------------------------------------------------------------------------- 1 | # Tip: To set environmental variables from an .env file, see https://bit.ly/2OdBca2 2 | 3 | shared: &shared 4 | REACT_PORT: 3000 5 | CLOUDINARY_CLOUD_NAME: eusy 6 | CLOUDINARY_API_KEY: 836838416199539 7 | 8 | development: 9 | <<: *shared 10 | DEBUG: '*' 11 | PUBLIC_URL: http://localhost:3000 12 | NODE_ENV: development 13 | APP_ENV: development 14 | 15 | stage: 16 | <<: *shared 17 | DEBUG: '*' 18 | PUBLIC_URL: https://stage.hookedjs.org 19 | NODE_ENV: production 20 | APP_ENV: stage 21 | 22 | production: 23 | <<: *shared 24 | DEBUG: 'error' 25 | PUBLIC_URL: https://hookedjs.org 26 | NODE_ENV: production 27 | APP_ENV: production 28 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "b807d0852e024a8f28c557ea7e6f788082a782c2764128cdb454fefcdd2ce35f": true, 3 | "956cfbbec487513e8402635ee7a8a7a6cff1eac694141c582df6d70e841467c4": true, 4 | "e4e46c7867d04c8de183a8eef5b2334e99eb92fe58469b30927cd75ae07f40bb": true, 5 | "a8d3da2b58cd0044bd5597b5dbc861dc8d6771a9ec59ecd226f4c2b9db2445e5": true, 6 | "1280815ac1591a236afaff25f4cf7469928b7fb76e73dc9d3408cb0e70f47f34": true, 7 | "2b8ac1a7ff406b1886096ce1e34e414d808afe49b6ca8c16ff0f1655f18fca21": true, 8 | "e6d82d57268a6c80e1b0ac781f5df7f97d2ebfbf0b51e0398079fed373e2ee71": true, 9 | "339e0e182ef495ee2fd447bed55ce153f70d9cfd6e3465c03dc5c20597514188": true, 10 | "442c81910584c9c31443bed0659a2b88023d484d69137a731b1778add32cd5fc": true, 11 | "2f05d2b3eb093fa1070fd6f76abeaa8cfb069565764bffc60209a3ea09641be0": true, 12 | "a0deb8e459e6dcb09ee4ad2909985b4ce7c2e758433a8323a3e98a924aa14ca3": true, 13 | "ebc7089f79e2c18b954686471d6e885f4de02807be52a908f3f4fc5a8563ffa1": true, 14 | "551d10ae5af6690557dadced83d71a37a5d40ff8c06f20a97a3405bc0b59b60c": true, 15 | "b4b7c2b4ea209c0f9487cb9ff71991b9ff9d7e9ffd4ead1b104b8fed4a7057af": true, 16 | "cabdbdd018d7f7bac5417971092880ee0fa8c50f0a9385e81a57eaa5029a0f5b": true, 17 | "bfc89c87f677b871edb29b5f0b80bc4b8f58d8acd7a8d2a3094f164e6ce443fc": true, 18 | "e6cc0ede32d31901ae92c2b185a29c49cbc834efe275f0834f52c3546cf8f642": true, 19 | "64f014c4451b1c95cd73aa59174f96dfb48cf5e6b11b53bdccdedf1484484c3d": true, 20 | "ead7cf9ffb3199149d9e649f190ae6e4afa5bb6e27daefcdae1abc73d745661b": true, 21 | "c863c1f31afdb25a2c234307d643498bcfa4df99b0cf7c2ab58942a67d33c022": true, 22 | "018573b9fe0654a44ea7080f3fa01e7f11864db5c4eddcc64dc914c672993051": true, 23 | "1797b4e4c4bfd76e52c6cc81cd92f4b06b1f53bd8a8ccd57d296108a2470ad9d": true, 24 | "503210b04a7dafc05f6a61827417e9427b5e36442eadde3b5da636aa8cc2381f": true, 25 | "475b26e2a6341639b740f91dc47ae3b3f5025fbb190cf4e8ca62cc45abb33335": true, 26 | "237836a5eeaab2a56b5c3ff7bed36190902f187701962209e5c2bc32f762c40f": true, 27 | "77bb2ac6a3e949b251f60ca669ed6495effea19b2bd9e647c7fa85a9e7fa4462": true, 28 | "ec35d55a965f10ba3c7367cd942702ffa566904ca242869a0c14cdbf7d4975f1": true, 29 | "4833c17682f9f7fed2ba600506061046aaf5dc025460b78dff2856b74b081801": true, 30 | "b3551c51ae7e6ac0e3b91f75d57e213cb2cb9691e459d3963e09cd0d172d31db": true, 31 | "a4db85b5a34f73e8ec86c6d8d31d42f88af07745200f704406d7c16450f2b475": true, 32 | "7457d7b713170d7afb31fb721f44abc3e158e91cf51cf9f47c6e732348da82db": true, 33 | "0120a5e621dc7c2270f5ddb3103ee1196213cb0e41c9d5608ad87582ed9fb1e1": true, 34 | "1d7337bea44932db559ec14d38387a6f83ffe4ef343391ebe7a01e9f2eabe245": true, 35 | "981c9377ecd4cb47d1731273f5208142f642ce1aa04f1b102109ae7cb35afdc6": true 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | *.orig.* 9 | web-build/ 10 | web-report/ 11 | .idea 12 | .env* 13 | !.env.yml 14 | graveyard 15 | git 16 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the main entry-point into the app, which is determined by expo.cli. 3 | * Tips: 4 | * - You should use non-functional components for higher level stuff, b/c fc's tend to break hot reload 5 | * - Use the assets features on this page to load fonts and stuff 6 | */ 7 | import { hot } from 'react-hot-loader'; 8 | import React from 'react'; 9 | import { Platform, View } from 'react-native'; 10 | import { ThemeProvider } from 'react-native-elements'; 11 | import { RoutesConfig } from './src/config/Routes.config'; 12 | import { AppLoadingScreen } from './src/components/screens/AppLoading.screen'; 13 | import { loadFonts } from './src/lib/AssetLoading'; 14 | import { SidebarSection } from './src/components/sections/Sidebar.section'; 15 | import { Router } from './src/components/lib/Routing'; 16 | 17 | import { Theme } from './src/config/Theme.config'; 18 | import { GlobalState } from './src/GlobalState'; 19 | import { Sleep } from './src/lib/Polyfills'; 20 | 21 | interface state { 22 | assetsLoaded: boolean; 23 | } 24 | 25 | class App extends React.PureComponent { 26 | constructor(props) { 27 | super(props); 28 | this.state = { 29 | assetsLoaded: false 30 | }; 31 | } 32 | 33 | static _loadAssets = async () => { 34 | const fontAssets = loadFonts({ 35 | // "Somefont": require('./font/somefont.ttf') 36 | }); 37 | await Promise.all([fontAssets]); 38 | while (!GlobalState.isHydrated) await Sleep(20); 39 | }; 40 | 41 | render() { 42 | if (!this.state.assetsLoaded) 43 | return ( 44 | this.setState({ assetsLoaded: true })} 47 | /> 48 | ); 49 | 50 | return ( 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | ); 61 | } 62 | } 63 | 64 | let HotApp = App; 65 | if (Platform.OS === 'web') HotApp = hot(module)(App); 66 | export default HotApp; 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EUSY: Expo Universal Starter 2 | 3 |

eusy

4 | 5 | A low-config, low-bloat, moderately opinionated starter/boilerplate for a universal web+mobile app built upon the managed Expo-CLI workflow. It comes with and demonstrates many commonly used UX patterns. All that with exceptional hot-reload support and < 150kb bundle. 6 | 7 | For this app, universal means the app works on every device that Expo supports: Web, IOS, and Android. 8 | 9 | This app is also the bases of the frontend of our full-stack starter, [HookedJS](https://github.com/hookedjs/hookedjs). Hence, the logo for EUSY is a simpler version of HookedJS. 10 | 11 | #### Demos 12 | - [Web](https://eusy.briandombrowski.now.sh/) 13 | - [Native](https://expo.io/@bdombro/eusy) 14 | 15 | #### Roadmap 16 | 17 | See Project: https://github.com/orgs/hookedjs/projects/1 18 | 19 | ## Features 20 | 21 | - [x] An optimized [Expo-CLI](https://docs.expo.io/versions/v34.0.0/workflow/expo-cli/) managed workflow easy developing and testing web and native 22 | - Highly functional, easy to extend development and production environments, including hot-moodule reloading for native AND web. Hot re-loading is delicate with react-native, and expo doesn't support hot reloading on web out of the box. 23 | - Easy and fully-automated development, sharing, publishing and notification features 24 | - Web Bundle analytics with [webpack-bundle-analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer), to hunt down bloated dependencies 25 | - Support .web.tsx and .native.tsx files, for easy platform specific overrides 26 | - Support for [styled-jsx](https://www.npmjs.com/package/styled-jsx) in .web.tsx files, for exceptional CSS-in-JSS 27 | - [x] Unified routing api extending [react-router](https://www.npmjs.com/package/react-router) 28 | - New unimodules: TextLink for links inside of Text elements 29 | - Native-style Animated transitions with [react-router-native-stack](https://www.npmjs.com/package/react-router-native-stack). 30 | - We forked it to add unimodule support 31 | - Note: Animations don't currently work on Web during development, but do in Production. See [Issue 3](https://github.com/hookedjs/eusy/issues/3). 32 | - [x] Universal UI Kit: [react-native-elements](https://www.npmjs.com/package/react-router). Enhanced for even easier theming. 33 | - [x] SEO thanks to [React Helmet](https://www.npmjs.com/package/react-helmet) 34 | - [x] Package Patches that fix some critical show-stopping bugs and enable better universal support, managed with [patch-package](https://www.npmjs.com/package/patch-package) 35 | - [x] Advanced Dot Environmental (.env) File Management and Support 36 | - [x] Typescript + TSLint + Prettier + Special TSLint sauce, using mostly default settings. 37 | - [x] [Hooks](https://reactjs.org/docs/hooks-intro.html) - Fully supported and preferred 38 | - [x] [MOBX](https://www.npmjs.com/package/mobx) - Popular, easy, distributed sharable app and component state 39 | - [x] Automated SVG to TSX file Conversion 40 | - [x] Git hooks that clean code, run tests and block broken commits, thanks to [Husky](https://www.npmjs.com/package/husky) and [Lint-staged](https://www.npmjs.com/package/lint-staged) 41 | - [x] Includes a production-grade web server, built on [Express](https://www.npmjs.com/package/express) 42 | - [x] Continuous Integration and Deployment thanks to [Zeit's Now Service](https://zeit.co/now) 43 | - [ ] [Jest](https://www.npmjs.com/package/jest) The most popular JS testing framework 44 | 45 | 46 | ## What's with the name? 47 | 48 | EUSY = An acronym of "Expo Universal Starter" with a 'Y' on the end. It's intentionally misspelled 'easy', so it's a play on words. 49 | 50 | 51 | ## Get Started 52 | 53 | First, ensure you have the system dependencies. As of now, MacOS is required in order to develop IOS apps. Also, this boilerplate will likely not work on Windows. 54 | 55 | 1. [Install homebrew](https://brew.sh/) 56 | 1. [Install nvm](https://github.com/nvm-sh/nvm#install--update-script) 57 | 1. [Install Docker](https://docs.docker.com/docker-for-mac/install/) 58 | 1. Install Xcode from the App Store and open it to accept the user agreement. 59 | 1. Follow [the official React Native instructions](https://facebook.github.io/react-native/docs/getting-started.html) to configure your machine for IOS and Android using the "React Native CLI Quickstart" tab, NOT the "Expo CLI Quickstart" tab. 60 | 61 | Then, install more dependencies 62 | 63 | ``` 64 | brew install gnu-sed 65 | brew install postgres 66 | brew install node 67 | brew install watchman 68 | brew tap AdoptOpenJDK/openjdk 69 | brew cask install adoptopenjdk8 70 | nvm install 10 71 | nvm use 10 72 | npm i -g typescript@3.4.5 yarn 73 | yarn 74 | ``` 75 | 76 | Now, you can run the development server and build the app. 77 | 78 | To run the development service run `npx expo start`. After that, you can launch web, IOS or Android using the prompts. 79 | 80 | Tips: 81 | 82 | - You can't start the Android simulator from the expo-cli. Instead, you have to start it first using Android Studio. Then, expo-cli can find it. 83 | - If the IOS emulator isn't running when you start it from expo, you may see an non-critical error. 84 | - How to open dev tools on simulator: CMD+D for menu in ios Sim, CMD+M for menu in android sim, shake on real devices. 85 | 86 | 87 | ## Publishing IOS 88 | 89 | You can easily publish to Expo using `npx expo:publish`. To publish to Apple, there are many steps. I recommend you do the following: 90 | 91 | 1. First publish your app to expo 92 | 1. Ensure you have an Apple developer subscription 93 | 1. Update app.json with your apps metadata if you haven't 94 | 1. Follow the [expo instructions](https://docs.expo.io/versions/v34.0.0/distribution/building-standalone-apps/) to bundle and deploy 95 | 96 | 97 | Tips: 98 | 99 | - Application Loader is included with XCode. To open, simple search for it using CMD+Space 100 | - To more easily manage your App Store metadata, you should consider [fastlane deliver](https://blog.expo.io/manage-app-store-metadata-in-expo-with-fastlane-deliver-1c00e06b73bf) 101 | 102 | 103 | ## Publishing Android 104 | 105 | You can easily publish to Expo using `npx expo:publish`. To publish to Google, there are many steps. I recommend you do the following: 106 | 107 | 1. First publish your app to expo 108 | 1. Ensure you have an Google Store developer account? 109 | 1. Update app.json with your apps metadata if you haven't 110 | 1. Follow the [expo instructions](https://docs.expo.io/versions/v34.0.0/distribution/building-standalone-apps/) to bundle and deploy 111 | 1. More coming soon 112 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "EUSY", 4 | "slug": "eusy", 5 | "description": "Expo Universal Starter: A low-config, low-bloat, moderately opinionated starter/boilerplate for a universal web+mobile app built upon the managed Expo-CLI workflow. It comes with and demonstrates many commonly used UX patterns. All that with exceptional hot-reload support and < 150kb bundle. More info at https://github.com/hookedjs/eusy", 6 | "privacy": "public", 7 | "sdkVersion": "34.0.0", 8 | "platforms": ["ios", "android", "web"], 9 | "version": "1.0.1", 10 | "icon": "./src/assets/img/logo-square.png", 11 | "splash": { 12 | "image": "./src/assets/img/splash.png", 13 | "resizeMode": "contain", 14 | "backgroundColor": "#ffffff" 15 | }, 16 | "updates": { 17 | "fallbackToCacheTimeout": 0 18 | }, 19 | "assetBundlePatterns": ["**/*"], 20 | "ios": { 21 | "supportsTablet": true, 22 | "bundleIdentifier": "com.hookedjs.eusy" 23 | }, 24 | "android": { 25 | "package": "com.hookedjs.eusy" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: ['styled-jsx/babel'] 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "EUSY", 3 | "public": true, 4 | "builds": [ 5 | { 6 | "src": "package.json", 7 | "use": "@now/static-build", 8 | "config": { "distDir": "web-build" } 9 | } 10 | ], 11 | "routes": [ 12 | { "handle": "filesystem" }, 13 | {"src": "/(.*)", "dest": "/"} 14 | ], 15 | "version": 2 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eusy", 3 | "description": "Expo Universal Starter: A low-config, low-bloat, moderately opinionated starter/boilerplate for a universal web+mobile app built upon the managed Expo-CLI workflow. It comes with and demonstrates many commonly used UX patterns. All that with exceptional hot-reload support and < 150kb bundle. More info at https://github.com/hookedjs/eusy.", 4 | "author": "Brian Dombrowski ", 5 | "version": "0.1.2", 6 | "license": "ISC", 7 | "private": true, 8 | "main": "node_modules/expo/AppEntry.js", 9 | "homepage": "https://github.com/hookedjs/eusy#readme", 10 | "bugs": { 11 | "url": "https://github.com/hookedjs/eusy/issues" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/hookedjs/eusy.git" 16 | }, 17 | "scripts": { 18 | "postinstall": "patch-package", 19 | "clean": "rimraf .env .env.json web-build", 20 | "gen:env": "dotenvi -f .env.yml -s ${APP_ENV:-development} && cat .env | npx envfile2json > .env.json", 21 | "gen:mockdb": "ts-node -T src/mockApi/dbseed.tsx", 22 | "gen:index": "node utils/create-indexes.js", 23 | "gen:svgTsx": "find src -name \"*.svg\" -exec ./utils/tsxify-svg.sh {} src/components/svgs \\;", 24 | "build": "yarn clean && run-p gen:env && BUILD_ENV=production expo build:web", 25 | "bootstrap": "yarn build", 26 | "start": "yarn gen:env && node server.js", 27 | "//start:dev": "yarn gen:env && expo start --web", 28 | "test1": "NODE_ENV=test jest", 29 | "test": "NODE_ENV=test jest --config jest/jest.web.json", 30 | "test:ios": "NODE_ENV=test jest --config jest/jest.ios.json", 31 | "test:android": "NODE_ENV=test jest --config jest/jest.android.json", 32 | "lint": "tslint --fix -p tsconfig.json -c tslint.json", 33 | "lint:fix": "tslint -p tsconfig.json -c tslint.json", 34 | "prettier": "prettier --write \"web-build\" \"src/**/*{.js,jsx,ts,tsx,md.json}\"" 35 | }, 36 | "browserslist": [ 37 | ">0.2%", 38 | "not dead", 39 | "not ie <= 11", 40 | "not op_mini all" 41 | ], 42 | "engineStrict": true, 43 | "engines": { 44 | "node": ">=10.15.3 <12.0.0", 45 | "npm": ">9999.0.0", 46 | "yarn": ">=1.9.4" 47 | }, 48 | "husky": { 49 | "hooks": { 50 | "pre-commit": "lint-staged" 51 | } 52 | }, 53 | "lint-staged": { 54 | "linters": { 55 | "*.{ts,tsx}": [ 56 | "prettier --write", 57 | "tslint --fix -p tsconfig.json -c tslint.json", 58 | "git add" 59 | ], 60 | "*.{js,jsx,json}": [ 61 | "prettier --write", 62 | "git add" 63 | ] 64 | }, 65 | "ignore": [ 66 | "**/generated/*" 67 | ] 68 | }, 69 | "prettier": { 70 | "printWidth": 100, 71 | "singleQuote": true 72 | }, 73 | "dependencies": { 74 | "@apollo/react-hooks": "^3.0.1", 75 | "@types/lunr": "^2.3.2", 76 | "apollo-boost": "^0.4.4", 77 | "bcryptjs": "^2.4.3", 78 | "deep-object-diff": "^1.1.0", 79 | "deepmerge": "^4.0.0", 80 | "expo": "^34.0.2", 81 | "expo-asset": "^6.0.0", 82 | "expo-font": "^6.0.0", 83 | "express": "^4.0.0", 84 | "gpl": "^0.0.1", 85 | "graphql": "^14.5.3", 86 | "lunr": "npm:lunr-mutable-indexes", 87 | "mobx": "^5.13.0", 88 | "mobx-react-lite": "^2.0.0-alpha.2", 89 | "mobx-utils": "^5.4.1", 90 | "morgan": "^1.9.0", 91 | "query-string": "^6.8.2", 92 | "react": "^16.8.6", 93 | "react-dom": "npm:@hot-loader/react-dom", 94 | "react-helmet": "^5.2.1", 95 | "react-hot-loader": "^4.12.10", 96 | "react-hover-observer": "^2.1.1", 97 | "react-native": "https://github.com/expo/react-native/archive/sdk-34.0.0.tar.gz", 98 | "react-native-animatable": "^1.3.2", 99 | "react-native-elements": "https://github.com/react-native-training/react-native-elements.git#49ca4d0f2d306d01d8e42d90bd57e2978d016d3c", 100 | "react-native-gesture-handler": "^1.3.0", 101 | "react-native-markdown-renderer": "^3.2.8", 102 | "react-native-scalable-image": "^0.5.1", 103 | "react-native-storage": "^1.0.1", 104 | "react-native-svg": "^9.5.3", 105 | "react-native-svg-uri-reborn": "^1.0.8", 106 | "react-native-web": "^0.11.4", 107 | "react-router-dom": "^5.0.1", 108 | "react-router-native": "^5.0.1", 109 | "react-router-native-stack": "https://github.com/hookedjs/react-router-native-stack.git#unimoduleUpgrade", 110 | "react-svg": "^10.0.14", 111 | "shrink-ray-current": "^4.0.0", 112 | "str_shorten": "^1.0.18", 113 | "styled-jsx": "^3.2.1", 114 | "use-react-router": "^1.0.7", 115 | "uuid": "^3.3.3" 116 | }, 117 | "devDependencies": { 118 | "@expo/webpack-config": "^0.7.2", 119 | "@types/jest": "^24.0.15", 120 | "@types/node": "^12.6.8", 121 | "@types/react": "^16.8.23", 122 | "@types/react-native": "^0.57.65", 123 | "@types/react-router-dom": "^4.3.4", 124 | "@types/react-router-native": "^4.2.4", 125 | "@types/react-svg": "^5.0.0", 126 | "@types/styled-jsx": "^2.2.8", 127 | "babel-jest": "24.7.1", 128 | "babel-preset-expo": "^6.0.0", 129 | "copy-webpack-plugin": "^5.0.4", 130 | "create-ts-index": "^1.10.2", 131 | "dotenv": "^8.0.0", 132 | "dotenvi": "^0.6.0", 133 | "envfile": "^3.0.0", 134 | "expo-cli": "^3.0.9", 135 | "faker": "^4.1.0", 136 | "glob": "^7.1.4", 137 | "husky": "^3.0.2", 138 | "jest": "^24.8.0", 139 | "lint-staged": "^8.2.1", 140 | "npm-run-all": "^4.1.5", 141 | "patch-package": "^6.1.2", 142 | "prettier": "^1.18.2", 143 | "react-test-renderer": "^16.8.6", 144 | "rimraf": "^2.6.3", 145 | "ts-jest": "^24.0.2", 146 | "ts-node": "^8.3.0", 147 | "tslint": "5.16.0", 148 | "tslint-config-prettier": "^1.18.0", 149 | "tslint-eslint-rules": "^5.4.0", 150 | "tslint-etc": "^1.6.0", 151 | "tslint-plugin-prettier": "^2.0.1", 152 | "typescript": "^3.4.5", 153 | "webpack-bundle-analyzer": "^3.4.1" 154 | }, 155 | "keywords": [ 156 | "react-native", 157 | "react-native-web", 158 | "react", 159 | "expo", 160 | "ios", 161 | "android", 162 | "universal", 163 | "expo-cli", 164 | "starter", 165 | "boilerplate", 166 | "react-router", 167 | "mobx" 168 | ] 169 | } 170 | -------------------------------------------------------------------------------- /patches/@types+lunr+2.3.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@types/lunr/index.d.ts b/node_modules/@types/lunr/index.d.ts 2 | index 9350d20..1374a8f 100644 3 | --- a/node_modules/@types/lunr/index.d.ts 4 | +++ b/node_modules/@types/lunr/index.d.ts 5 | @@ -353,6 +353,13 @@ declare namespace lunr { 6 | * @param serializedIndex - A previously serialized lunr.Index 7 | */ 8 | static load(serializedIndex: object): Index; 9 | + 10 | + /** 11 | + * Patch to support lunr-mutable-indexes features 12 | + */ 13 | + add: (any) => null; 14 | + remove: ({id: string}) => null; 15 | + update: (any) => null; 16 | } 17 | 18 | /** 19 | -------------------------------------------------------------------------------- /patches/react-native-elements+1.1.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-native-elements/src/input/Input.js b/node_modules/react-native-elements/src/input/Input.js 2 | index e727fad..b28b153 100644 3 | --- a/node_modules/react-native-elements/src/input/Input.js 4 | +++ b/node_modules/react-native-elements/src/input/Input.js 5 | @@ -76,6 +76,8 @@ class Input extends React.Component { 6 | labelStyle, 7 | labelProps, 8 | theme, 9 | + updateTheme, 10 | + replaceTheme, 11 | ...attributes 12 | } = this.props; 13 | 14 | diff --git a/node_modules/react-native-elements/src/searchbar/SearchBar-default.js b/node_modules/react-native-elements/src/searchbar/SearchBar-default.js 15 | index 39b7ebc..3712342 100644 16 | --- a/node_modules/react-native-elements/src/searchbar/SearchBar-default.js 17 | +++ b/node_modules/react-native-elements/src/searchbar/SearchBar-default.js 18 | @@ -75,6 +75,7 @@ class SearchBar extends React.Component { 19 | showLoading, 20 | loadingProps, 21 | placeholderTextColor = theme.colors.grey3, 22 | + onClear, 23 | ...attributes 24 | } = rest; 25 | 26 | diff --git a/node_modules/react-native-elements/src/text/Text.js b/node_modules/react-native-elements/src/text/Text.js 27 | index b1b26c2..c738e34 100644 28 | --- a/node_modules/react-native-elements/src/text/Text.js 29 | +++ b/node_modules/react-native-elements/src/text/Text.js 30 | @@ -17,6 +17,9 @@ const TextElement = props => { 31 | h2Style, 32 | h3Style, 33 | h4Style, 34 | + theme, 35 | + updateTheme, 36 | + replaceTheme, 37 | ...rest 38 | } = props; 39 | 40 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const logger = require('morgan'); 3 | const shrinkRay = require('shrink-ray-current'); 4 | 5 | const app = express(); 6 | 7 | app.use(logger('dev')); 8 | 9 | app.use(shrinkRay()); 10 | 11 | app.use(express.static('web-build', { index: 'index.html' })); 12 | 13 | // Handle 404 14 | app.use(function(req, res) { 15 | res.sendFile('MockOrm.tsx.html', { root: 'web-build' }); 16 | }); 17 | 18 | // error handler 19 | app.use(function(err, req, res, next) { 20 | // set locals, only providing error in development 21 | res.locals.message = err.message; 22 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 23 | 24 | throw err; 25 | }); 26 | 27 | const port = process.env.PORT || '3000'; 28 | 29 | app.listen(port, () => { 30 | console.log('Server listening on:', port); 31 | }); 32 | -------------------------------------------------------------------------------- /src/@types/ArgumentTypes.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extract the types of a function. 3 | * 4 | * In the case that function arg types are declared inline, you can use this type to extract them. 5 | * 6 | * Example: 7 | * 8 | * const f = (arg1: string, arg2: string) => null; 9 | * type arg1Type = ArgumentTypes[0]; 10 | * type arg2Type = ArgumentTypes[1]; 11 | * 12 | * or 13 | * 14 | * const f = ({prop1: string, prop2: string}) => null; 15 | * type prop1Type = ArgumentTypes[0]['prop1']; 16 | * type prop2Type = ArgumentTypes[0]['prop2']; 17 | * 18 | */ 19 | 20 | declare type ArgumentTypes = F extends (...args: infer A) => any ? A : never; 21 | -------------------------------------------------------------------------------- /src/@types/Omit.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * From T pick all properties except the set of properties K 3 | * 4 | * This allows you to exclude an attribute from an interface or type 5 | * 6 | * Example: 7 | * 8 | * type ButtonStripped = Omit; 9 | * export interface ButtonPropsExt extends ButtonStripped {} 10 | * 11 | * or 12 | * 13 | * export interface ButtonPropsExt extends Omit {} 14 | * 15 | */ 16 | 17 | declare type Omit = Pick>; 18 | -------------------------------------------------------------------------------- /src/@types/RecursivePartial.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Like Partial, but recursive! 3 | * 4 | * Borrowed from react-native-elements 5 | */ 6 | declare type RecursivePartial = { [P in keyof T]?: RecursivePartial }; 7 | -------------------------------------------------------------------------------- /src/@types/graphql-tag.d.ts: -------------------------------------------------------------------------------- 1 | export type gqlReturnType = { 2 | definitions: { 3 | operation: string; // e.g. 'mutation' | 'query', 4 | name: { 5 | value: string; // e.g. "CreateUser" | "UpdateUser" 6 | }; 7 | variableDefinitions: { 8 | // like in mutations, you supply variables 9 | variable: { 10 | name: { 11 | value: string; // e.g. 'email' | 'nameGiven' 12 | }; 13 | }; 14 | type: { 15 | type: { 16 | name: { 17 | value: string; // e.g. "String!" 18 | }; 19 | }; 20 | }; 21 | }[]; 22 | selectionSet: { 23 | selections: { 24 | name: { 25 | value: string; // e.g. 'user' | 'users' | 'createUser' | 'updateUser' 26 | }; 27 | arguments: { 28 | name: { 29 | value: string; // e.g. 'where' | 'data' 30 | }; 31 | value: { 32 | fields: { 33 | name: { 34 | value: string; // e.g. 'id' | 'email', // these are where filters 35 | }; 36 | value: { 37 | value: string; // e.g. 'bdombro@gmail.com' | 'email' if a variable like $email 38 | }; 39 | }[]; 40 | }; 41 | }[]; 42 | selectionSet: { 43 | // this is what's returned 44 | selections: { 45 | name: { 46 | value: string; // e.g. 'id' | 'email' | 'nameFirst' | 'nameLast' 47 | }; 48 | }[]; 49 | }; 50 | }[]; 51 | }; 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /src/@types/graphql-tags.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.gql'; 2 | declare module '*.graphql'; 3 | -------------------------------------------------------------------------------- /src/@types/isbot/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'isbot' { 2 | export default function(userAgent: string): boolean; 3 | } 4 | -------------------------------------------------------------------------------- /src/@types/media.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg'; 2 | declare module '*.JPG'; 3 | declare module '*.jpeg'; 4 | declare module '*.JPEG'; 5 | declare module '*.svg'; 6 | declare module '*.SVG'; 7 | declare module '*.png'; 8 | declare module '*.PNG'; 9 | declare module '*.gif'; 10 | declare module '*.GIF'; 11 | -------------------------------------------------------------------------------- /src/GlobalState.tsx: -------------------------------------------------------------------------------- 1 | import { action, observable, set } from 'mobx'; 2 | import { Dimensions } from 'react-native'; 3 | import { getViewportInfo } from './lib/getViewportInfo'; 4 | import { MobxPersistClass } from './lib/MobxPersistClass'; 5 | 6 | class GlobalStateClass { 7 | // persistedFields = ['user', 'sidebarToggled']; 8 | isHydrated = false; 9 | 10 | @observable 11 | user = { 12 | id: '', 13 | token: '', 14 | roles: [] 15 | }; 16 | 17 | @action 18 | logout = async () => { 19 | this.user = { 20 | id: '', 21 | token: '', 22 | roles: [] 23 | }; 24 | }; 25 | 26 | @observable 27 | currentPageTitle = ''; 28 | 29 | @observable 30 | sidebarToggled = true; 31 | @observable 32 | sidebarComponent = null; 33 | 34 | @observable 35 | search: string = ''; 36 | 37 | @observable 38 | viewportInfo = getViewportInfo(); 39 | @action 40 | refreshViewportInfo = () => { 41 | const width = Dimensions.get('window').width; 42 | const height = Dimensions.get('window').height; 43 | if (width != this.viewportInfo.width || height != this.viewportInfo.height) 44 | set(this.viewportInfo, getViewportInfo()); 45 | }; 46 | } 47 | export const GlobalState = new GlobalStateClass(); 48 | MobxPersistClass(GlobalState); 49 | 50 | setInterval(() => { 51 | GlobalState.refreshViewportInfo(); 52 | }, 400); 53 | -------------------------------------------------------------------------------- /src/assets/img/brian-eus-author.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hookedjs/eusy/3ce0b6530bcc9b5873694e0c3876438b9842e2f1/src/assets/img/brian-eus-author.jpg -------------------------------------------------------------------------------- /src/assets/img/logo-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hookedjs/eusy/3ce0b6530bcc9b5873694e0c3876438b9842e2f1/src/assets/img/logo-icon.png -------------------------------------------------------------------------------- /src/assets/img/logo-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/img/logo-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hookedjs/eusy/3ce0b6530bcc9b5873694e0c3876438b9842e2f1/src/assets/img/logo-square.png -------------------------------------------------------------------------------- /src/assets/img/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hookedjs/eusy/3ce0b6530bcc9b5873694e0c3876438b9842e2f1/src/assets/img/preview.png -------------------------------------------------------------------------------- /src/assets/img/space.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hookedjs/eusy/3ce0b6530bcc9b5873694e0c3876438b9842e2f1/src/assets/img/space.jpg -------------------------------------------------------------------------------- /src/assets/img/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hookedjs/eusy/3ce0b6530bcc9b5873694e0c3876438b9842e2f1/src/assets/img/splash.png -------------------------------------------------------------------------------- /src/components/elements/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Avatar as RNEAvatar, AvatarProps } from 'react-native-elements'; 3 | import { ImageURISource } from 'react-native'; 4 | 5 | export const Avatar = ({ title, source, ...rest }: AvatarProps) => { 6 | source = source as ImageURISource; 7 | if (source && source.uri) return ; 8 | else return ; 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/elements/index.tsx: -------------------------------------------------------------------------------- 1 | export { 2 | Badge, 3 | Button, 4 | ButtonGroup, 5 | Card, 6 | Input, 7 | ListItem, 8 | PricingCard, 9 | Tooltip, 10 | SocialIcon, 11 | Text, 12 | Divider, 13 | CheckBox, 14 | SearchBar, 15 | Icon, 16 | colors, 17 | getIconType, 18 | registerCustomIconType, 19 | normalize, 20 | Tile, 21 | Slider, 22 | Rating, 23 | AirbnbRating, 24 | Header, 25 | Overlay, 26 | ThemeProvider, 27 | ThemeConsumer, 28 | ThemeContext, 29 | withBadge, 30 | withTheme, 31 | Image 32 | } from 'react-native-elements'; 33 | 34 | export { Avatar } from './Avatar'; 35 | -------------------------------------------------------------------------------- /src/components/lib/Helmet.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Helmet does not apply for mobile. 3 | */ 4 | import React from 'react'; 5 | import { GlobalState } from '../../GlobalState'; 6 | 7 | export class Helmet extends React.PureComponent<{ 8 | title?: string; 9 | description?: string; 10 | }> { 11 | render() { 12 | GlobalState.currentPageTitle = this.props.title; 13 | return <>; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/lib/Helmet.web.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet as HelmetCore } from 'react-helmet'; 3 | import { AppName, AppDescription } from '../../config/App.config'; 4 | import { GlobalState } from '../../GlobalState'; 5 | 6 | export class Helmet extends React.Component<{ 7 | title?: string; 8 | description?: string; 9 | }> { 10 | componentDidMount() { 11 | // Delete strange extra meta description that must be added by React or something 12 | const e = document.querySelector('meta[name=description]:not([data-react-helmet=true])'); 13 | if (e) e.parentNode.removeChild(e); 14 | } 15 | 16 | render() { 17 | // const url = `${config.baseUrl}${location.pathname}${location.search}${location.hash}`; 18 | const { title, description } = this.props; 19 | GlobalState.currentPageTitle = title; 20 | 21 | return ( 22 | 23 | 24 | {AppName} 25 | {title ? ` | ${title}` : ''} 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | } 34 | 35 | // Stateless has issues with render infinite loop. Use PureComponent instead 36 | // Ref: https://github.com/facebook/react/issues/5677 37 | // export const HelmetDefault = ({title}: {title?: string}) => { 38 | // // const url = `${config.baseUrl}${location.pathname}${location.search}${location.hash}`; 39 | // return ( 40 | // 41 | // 42 | // {Config.appName} 43 | // {title ? ` | ${title}` : ""} 44 | // 45 | // 46 | // 47 | // ); 48 | // }; 49 | -------------------------------------------------------------------------------- /src/components/lib/HoverObserver.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export const HoverObserver = ({ children }) => <>{children({ isHovering: false })}; 3 | -------------------------------------------------------------------------------- /src/components/lib/HoverObserver.web.tsx: -------------------------------------------------------------------------------- 1 | export { default as HoverObserver } from 'react-hover-observer'; 2 | -------------------------------------------------------------------------------- /src/components/lib/Routing.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Linking } from 'expo'; 3 | import { toJS } from 'mobx'; 4 | import { Text, TextProps, View } from 'react-native'; 5 | import { ThemeContext } from 'react-native-elements'; 6 | import { 7 | NativeRouter as Router, 8 | Link as RRNLink, 9 | matchPath, 10 | Redirect, 11 | Route as RRNRoute, 12 | Switch, 13 | withRouter, 14 | RouteComponentProps, 15 | RouteProps, 16 | LinkProps 17 | } from 'react-router-native'; 18 | import Stack from 'react-router-native-stack'; 19 | import useRouter from 'use-react-router'; 20 | import { GlobalState } from '../../GlobalState'; 21 | import { ArrayIntersection } from '../../lib/Polyfills'; 22 | import { TouchableOpacity } from './Touchables'; 23 | import { ThemeType } from '../../config/Theme.config'; 24 | 25 | // Extend Route to sync sidebar 26 | class Route extends React.PureComponent< 27 | RouteProps & { 28 | headerComponent?: React.ComponentType> | React.ComponentType; 29 | footerComponent?: React.ComponentType> | React.ComponentType; 30 | footerEndComponent?: React.ComponentType> | React.ComponentType; 31 | sidebarComponent?: React.ComponentType> | React.ComponentType; 32 | animationType?: string; 33 | requiresRole?: string[]; 34 | } 35 | > { 36 | // If page permissions fail, redirect to login 37 | redirectIfUnderprivileged = currentPath => { 38 | if ( 39 | !GlobalState.user.roles.includes('admin') && 40 | this.props.requiresRole && 41 | this.props.requiresRole.length && 42 | !ArrayIntersection(toJS(GlobalState.user.roles), this.props.requiresRole).length 43 | ) { 44 | console.log(`Current user fails permissions for ${this.props.path}`); 45 | return ; 46 | } 47 | }; 48 | 49 | render() { 50 | // Create routeProps to be passed to RRNRoute 51 | let routeProps = { ...this.props }; 52 | delete routeProps.component; 53 | delete routeProps.footerEndComponent; 54 | delete routeProps.sidebarComponent; 55 | delete routeProps.requiresRole; 56 | 57 | // Set the sidebar component 58 | GlobalState.sidebarComponent = this.props.sidebarComponent; 59 | 60 | return ( 61 | ( 63 | 72 | {this.redirectIfUnderprivileged(routerProps.match.path)} 73 | 74 | 75 | )} 76 | {...routeProps} 77 | /> 78 | ); 79 | } 80 | } 81 | 82 | const Link = ({ to, onPress, ...props }: LinkProps) => { 83 | const { history } = useRouter(); 84 | 85 | return ( 86 | { 88 | if (onPress) await onPress(e); 89 | if (typeof to === 'string' && to[0] === '#') void 0; 90 | else if (typeof to === 'string' && to[0] !== '/') Linking.openURL(to); 91 | else history.push(to as string); 92 | }} 93 | > 94 | 95 | 96 | ); 97 | }; 98 | 99 | const TextLink = ({ to, onPress, style, ...props }: LinkProps & TextProps) => { 100 | const { history } = useRouter(); 101 | const theme = useContext(ThemeContext).theme as ThemeType; 102 | 103 | return ( 104 | { 106 | if (onPress) onPress(e); 107 | console.log(typeof to); 108 | if (typeof to === 'string' && to[0] === '#') void 0; 109 | else if (typeof to === 'string' && to[0] !== '/') Linking.openURL(to); 110 | else history.push(to as string); 111 | }} 112 | style={{ 113 | textDecorationLine: 'underline', 114 | color: theme.colors.primary, 115 | // @ts-ignore: style spread works but typescript gets cranky 116 | ...style 117 | }} 118 | {...props} 119 | /> 120 | ); 121 | }; 122 | 123 | export { 124 | Link, 125 | matchPath, 126 | Route, 127 | Redirect, 128 | Router, 129 | RouteComponentProps, 130 | Switch, 131 | Stack, 132 | TextLink, 133 | withRouter, 134 | useRouter 135 | }; 136 | -------------------------------------------------------------------------------- /src/components/lib/Routing.web.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { toJS } from 'mobx'; 3 | import { ScrollView } from 'react-native'; 4 | import { TextProps, ThemeContext } from 'react-native-elements'; 5 | import { 6 | BrowserRouter as Router, 7 | matchPath, 8 | Link as RRDLink, 9 | Redirect, 10 | Route as RRNRoute, 11 | RouteProps, 12 | Switch, 13 | withRouter, 14 | RouteComponentProps, 15 | LinkProps 16 | } from 'react-router-dom'; 17 | import RRNSStack from 'react-router-native-stack'; 18 | import useRouter from 'use-react-router'; 19 | import { GlobalState } from '../../GlobalState'; 20 | import { ArrayIntersection } from '../../lib/Polyfills'; 21 | import { Helmet } from './Helmet'; 22 | import { ThemeType } from '../../config/Theme.config'; 23 | 24 | // Extend Route to sync sidebar and wrap in scrollview 25 | class Route extends React.PureComponent< 26 | RouteProps & { 27 | headerComponent?: React.ComponentType> | React.ComponentType; 28 | footerComponent?: React.ComponentType> | React.ComponentType; 29 | footerEndComponent?: React.ComponentType> | React.ComponentType; 30 | sidebarComponent?: React.ComponentType> | React.ComponentType; 31 | animationType?: string; 32 | requiresRole?: string[]; 33 | } 34 | > { 35 | // If page permissions fail, redirect to login 36 | redirectIfUnderprivileged = currentPath => { 37 | if ( 38 | !GlobalState.user.roles.includes('admin') && 39 | this.props.requiresRole && 40 | this.props.requiresRole.length && 41 | !ArrayIntersection(toJS(GlobalState.user.roles), this.props.requiresRole).length 42 | ) { 43 | console.log(`Current user fails permissions for ${this.props.path}`); 44 | return ; 45 | } 46 | }; 47 | 48 | render() { 49 | // Create routeProps to be passed to RRNRoute 50 | let routeProps = { ...this.props }; 51 | delete routeProps.component; 52 | delete routeProps.footerEndComponent; 53 | delete routeProps.sidebarComponent; 54 | delete routeProps.requiresRole; 55 | 56 | // Set the sidebar component 57 | GlobalState.sidebarComponent = this.props.sidebarComponent; 58 | 59 | // Have to include the headercomponent on development mode for now because development mode 60 | // uses Switch instead of Stack, due to an HMR bug (see Stack declaration for more info). 61 | return ( 62 | ( 64 | <> 65 | {this.props.headerComponent && process.env.NODE_ENV !== 'production' && ( 66 | 67 | )} 68 | 78 | {this.redirectIfUnderprivileged(routerProps.match.path)} 79 | 80 | 81 | 82 | {this.props.footerEndComponent && } 83 | 84 | 85 | )} 86 | {...routeProps} 87 | /> 88 | ); 89 | } 90 | } 91 | 92 | const Link = ({ 93 | style = {}, 94 | onPress = () => null, 95 | ...props 96 | }: LinkProps & { 97 | onPress: () => any; 98 | }) => { 99 | style.textDecorationLine = 'none'; 100 | 101 | if (typeof props.to === 'string' && props.to[0] === '#') { 102 | return ; 103 | } else if (typeof props.to === 'string' && props.to[0] !== '/') { 104 | return ; 105 | } else { 106 | return ; 107 | } 108 | }; 109 | 110 | const TextLink = ({ 111 | onPress = () => null, 112 | ...props 113 | }: LinkProps & 114 | TextProps & { 115 | onPress: () => any; 116 | }) => { 117 | const theme = useContext(ThemeContext).theme as ThemeType; 118 | 119 | if (typeof props.to === 'string' && props.to[0] === '#') { 120 | return ( 121 | 133 | ); 134 | } else if (typeof props.to === 'string' && props.to[0] !== '/') { 135 | return ( 136 | 148 | ); 149 | } else { 150 | return ( 151 |
152 | 153 | 154 | 162 |
163 | ); 164 | } 165 | }; 166 | 167 | // TODO: Get HMR working with Stack. 168 | const Stack = process.env.NODE_ENV === 'production' ? RRNSStack : Switch; 169 | 170 | export { Link, matchPath, Route, Redirect, Router, Switch, Stack, TextLink, withRouter, useRouter }; 171 | -------------------------------------------------------------------------------- /src/components/lib/Touchables.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * RNGH is more reliable and smoother than straight up RN, but doesn't work on web. 3 | */ 4 | export { TouchableOpacity } from 'react-native-gesture-handler'; 5 | -------------------------------------------------------------------------------- /src/components/lib/Touchables.web.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * RNGH is more reliable and smoother than straight up RN, but doesn't work on web. 3 | */ 4 | 5 | export { TouchableOpacity } from 'react-native'; 6 | 7 | // import { withRouter } from './Routing'; 8 | 9 | // export const TouchableOpacity => withRouter({ 10 | // history, 11 | // to, 12 | // onPress, 13 | // ...props 14 | // }) => { 15 | // if (to) onPress = () => history.push(to); 16 | // return 17 | // } 18 | -------------------------------------------------------------------------------- /src/components/modules/Logo.module.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GlobalState } from '../../GlobalState'; 3 | import { LogoIcon } from '../svgs/LogoIcon'; 4 | import { SvgProps } from 'react-native-svg'; 5 | 6 | // Leaving props type as any until we get an SVG logo, then we'll switch it to svg. 7 | export const LogoModule = ({ width, height, ...props }: Partial) => { 8 | return ( 9 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/modules/Sidebar.module.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { gql } from 'apollo-boost'; 3 | import { Feather } from '@expo/vector-icons'; 4 | import { observer } from 'mobx-react-lite'; 5 | import { Text, View } from 'react-native'; 6 | import { ThemeContext } from '../elements'; 7 | import { GlobalState } from '../../GlobalState'; 8 | import { useQuery } from '../../mockApi/hooks/useQuery'; 9 | import { NotificationType } from '../../model/notifications/type'; 10 | import { HoverObserver } from '../lib/HoverObserver'; 11 | import { Link, useRouter } from '../lib/Routing'; 12 | import { LogoModule } from './Logo.module'; 13 | import { ThemeType } from '../../config/Theme.config'; 14 | 15 | const NOTIFICATION_COUNT = gql` 16 | query($id: string) { 17 | notifications(where: { id: $id, unread: true }) { 18 | id 19 | } 20 | } 21 | `; 22 | 23 | export const SidebarModule = observer(() => { 24 | const theme = useContext(ThemeContext).theme as ThemeType; 25 | const notificationQuery = useQuery(NOTIFICATION_COUNT, { 26 | variables: { userId: GlobalState.user.id }, 27 | pollInterval: 2000 28 | }); 29 | 30 | const SidebarHeader = () => { 31 | const theme = useContext(ThemeContext).theme as ThemeType; 32 | 33 | return ( 34 | ( 36 | 37 | 45 | 46 | 47 | EUSY 48 | 49 | 50 | 51 | )} 52 | /> 53 | ); 54 | }; 55 | 56 | const SidebarMenuItem = ({ 57 | to, 58 | text, 59 | featherIconName, 60 | showActivityBubble 61 | }: { 62 | to: any; 63 | text: string; 64 | featherIconName: string; 65 | showActivityBubble?: boolean; 66 | }) => { 67 | const { location } = useRouter(); 68 | const theme = useContext(ThemeContext).theme as ThemeType; 69 | const isActive = location.pathname.startsWith(to); 70 | 71 | return ( 72 | ( 74 | 75 | 83 | 84 | 85 | {!!showActivityBubble && ( 86 | 100 | )} 101 | 102 | 103 | {text} 104 | 105 | 106 | 107 | )} 108 | /> 109 | ); 110 | }; 111 | 112 | if (notificationQuery.error.message || notificationQuery.error.graphQLErrors.length) { 113 | console.dir(notificationQuery.error); 114 | return <>; 115 | } 116 | if (!Array.isArray(notificationQuery.data)) return <>; 117 | 118 | return ( 119 | 126 | 127 | {GlobalState.viewportInfo.isLarge && } 128 | 129 | 135 | 136 | 137 | 138 | 139 | 140 | ); 141 | }); 142 | -------------------------------------------------------------------------------- /src/components/screens/AppLoading.screen.tsx: -------------------------------------------------------------------------------- 1 | export { AppLoading as AppLoadingScreen } from 'expo'; 2 | -------------------------------------------------------------------------------- /src/components/screens/AppLoading.screen.web.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { View, StyleSheet, ActivityIndicator } from 'react-native'; 3 | 4 | const styles = StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | alignItems: 'center', 8 | justifyContent: 'center', 9 | padding: 10 10 | } 11 | }); 12 | 13 | export const AppLoadingScreen = ({ 14 | startAsync, 15 | onError = () => null, 16 | onFinish = () => null, 17 | ActivityIndicatorProps = {} 18 | }: { 19 | startAsync: () => any; 20 | onError?: () => any; 21 | onFinish?: () => any; 22 | ActivityIndicatorProps?: any; 23 | }) => { 24 | useEffect(() => { 25 | if (startAsync) 26 | Promise.resolve(startAsync()) 27 | .then(onFinish) 28 | .catch(onError); 29 | else onFinish(); 30 | }, [false]); 31 | 32 | return ( 33 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/screens/Home.screen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { ScrollView, View } from 'react-native'; 3 | import { Button, Text, ThemeContext } from '../elements'; 4 | import { StyleSheet } from 'react-native'; 5 | import { Helmet } from '../lib/Helmet'; 6 | import { Link, TextLink } from '../lib/Routing'; 7 | import { getColors } from '../../config/Theme.config'; 8 | import { LogoModule } from '../modules/Logo.module'; 9 | 10 | export const HomeScreen = () => { 11 | const title = 'Home'; 12 | const { updateTheme } = useContext(ThemeContext); 13 | 14 | return ( 15 | <> 16 | 17 | 18 | 27 | 28 | 29 | Welcome to EUSA 30 | 31 | 32 | Expo Universal Starter: A low-config, low-bloat, moderately opinionated 33 | starter/boilerplate for a universal web+mobile app built upon the managed Expo-CLI 34 | workflow. 35 | 36 | 37 | It comes with and demonstrates many commonly used UX patterns. All that with exceptional 38 | hot-reload support and less 150kb bundle. More info at{' '} 39 | 40 | https://github.com/hookedjs/eusy 41 | 42 | . 43 | 44 | 45 |