From e5021187e96b78b53203bd95d08d6818aea47d17 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Wed, 14 Jul 2021 15:10:31 +0200 Subject: New Ignite 7.0.6 app --- .gitignore | 83 + .prettierignore | 6 + .solidarity | 24 + App.js | 6 + README.md | 145 + app.json | 26 + app/app.tsx | 81 + app/components/auto-image/auto-image.story.tsx | 31 + app/components/auto-image/auto-image.tsx | 46 + app/components/bullet-item/bullet-item.tsx | 41 + app/components/button/button.presets.ts | 54 + app/components/button/button.props.ts | 35 + app/components/button/button.story.tsx | 33 + app/components/button/button.tsx | 36 + app/components/checkbox/checkbox.props.ts | 44 + app/components/checkbox/checkbox.story.tsx | 121 + app/components/checkbox/checkbox.tsx | 53 + app/components/form-row/form-row.presets.ts | 71 + app/components/form-row/form-row.props.tsx | 23 + app/components/form-row/form-row.story.tsx | 107 + app/components/form-row/form-row.tsx | 13 + app/components/header/header.props.ts | 45 + app/components/header/header.story.tsx | 43 + app/components/header/header.tsx | 61 + app/components/icon/icon.props.ts | 21 + app/components/icon/icon.story.tsx | 19 + app/components/icon/icon.tsx | 19 + app/components/icon/icons/arrow-left.png | Bin 0 -> 329 bytes app/components/icon/icons/arrow-left@2x.png | Bin 0 -> 329 bytes app/components/icon/icons/bullet.png | Bin 0 -> 204 bytes app/components/icon/icons/bullet@2x.png | Bin 0 -> 204 bytes app/components/icon/icons/index.ts | 6 + app/components/index.ts | 12 + app/components/screen/screen.presets.ts | 66 + app/components/screen/screen.props.ts | 46 + app/components/screen/screen.tsx | 66 + app/components/switch/switch.props.ts | 39 + app/components/switch/switch.story.tsx | 110 + app/components/switch/switch.tsx | 114 + app/components/text-field/text-field.story.tsx | 159 + app/components/text-field/text-field.tsx | 98 + app/components/text/text.presets.ts | 48 + app/components/text/text.props.ts | 37 + app/components/text/text.story.tsx | 92 + app/components/text/text.tsx | 28 + app/components/wallpaper/bg.png | Bin 0 -> 56176 bytes app/components/wallpaper/bg@2x.png | Bin 0 -> 203224 bytes app/components/wallpaper/wallpaper.presets.ts | 34 + app/components/wallpaper/wallpaper.props.ts | 19 + app/components/wallpaper/wallpaper.story.tsx | 16 + app/components/wallpaper/wallpaper.tsx | 25 + app/config/env.js | 1 + app/i18n/en.json | 34 + app/i18n/i18n.ts | 22 + app/i18n/index.ts | 3 + app/i18n/ja.json | 7 + app/i18n/translate.ts | 11 + app/models/character-store/character-store.test.ts | 7 + app/models/character-store/character-store.ts | 37 + app/models/character/character.test.ts | 10 + app/models/character/character.ts | 17 + app/models/environment.ts | 40 + app/models/extensions/with-environment.ts | 17 + app/models/extensions/with-root-store.ts | 17 + app/models/index.ts | 5 + app/models/root-store/root-store-context.ts | 22 + app/models/root-store/root-store.ts | 20 + app/models/root-store/setup-root-store.ts | 55 + app/navigators/index.ts | 4 + app/navigators/main-navigator.tsx | 57 + app/navigators/navigation-utilities.tsx | 127 + app/navigators/root-navigator.tsx | 59 + app/screens/demo/demo-list-screen.tsx | 86 + app/screens/demo/demo-screen.tsx | 181 + app/screens/demo/heart.png | Bin 0 -> 204 bytes app/screens/demo/heart@2x.png | Bin 0 -> 377 bytes app/screens/demo/logo-ignite.png | Bin 0 -> 9427 bytes app/screens/demo/logo-ignite@2x.png | Bin 0 -> 20283 bytes app/screens/index.ts | 4 + app/screens/welcome/bowser.png | Bin 0 -> 33502 bytes app/screens/welcome/bowser@2x.png | Bin 0 -> 79259 bytes app/screens/welcome/welcome-screen.tsx | 118 + app/services/api/api-config.ts | 27 + app/services/api/api-problem.test.ts | 72 + app/services/api/api-problem.ts | 74 + app/services/api/api.ts | 102 + app/services/api/api.types.ts | 13 + app/services/api/character-api.ts | 37 + app/services/api/index.ts | 2 + app/services/reactotron/index.ts | 1 + app/services/reactotron/reactotron-config.ts | 30 + app/services/reactotron/reactotron.ts | 181 + app/services/reactotron/tron.ts | 2 + app/services/reactotron/tron.web.ts | 2 + app/theme/color.ts | 64 + app/theme/fonts/index.ts | 12 + app/theme/index.ts | 4 + app/theme/palette.ts | 11 + app/theme/spacing.ts | 41 + app/theme/timing.ts | 6 + app/theme/typography.ts | 31 + app/utils/delay.ts | 6 + app/utils/ignore-warnings.ts | 10 + app/utils/keychain.ts | 63 + app/utils/storage/index.ts | 1 + app/utils/storage/storage.test.ts | 39 + app/utils/storage/storage.ts | 79 + app/utils/validate.ts | 77 + assets/fonts/custom-fonts.md | 24 + assets/images/adaptive-icon.png | Bin 0 -> 17547 bytes assets/images/favicon.png | Bin 0 -> 1466 bytes assets/images/icon.png | Bin 0 -> 22380 bytes assets/images/splash.png | Bin 0 -> 48478 bytes babel.config.js | 15 + bin/downloadExpoApp.sh | 30 + bin/postInstall | 40 + bin/setup | 112 + e2e/README.md | 66 + e2e/config.json | 4 + e2e/firstTest.spec.js | 19 + e2e/init.js | 19 + e2e/reload.js | 2 + ignite/templates/component/NAME.story.tsx.ejs | 15 + ignite/templates/component/NAME.tsx.ejs | 37 + ignite/templates/model/NAME.test.ts.ejs | 7 + ignite/templates/model/NAME.ts.ejs | 16 + ignite/templates/navigator/NAME-navigator.tsx.ejs | 7 + ignite/templates/screen/NAME-screen.tsx.ejs | 25 + package.json | 200 + react-native.config.js | 3 + storybook/index.ts | 4 + storybook/storybook-registry.ts | 10 + storybook/storybook.tsx | 31 + storybook/toggle-storybook.tsx | 51 + storybook/toggle-storybook.web.tsx | 30 + storybook/views/index.ts | 3 + storybook/views/story-screen.tsx | 15 + storybook/views/story.tsx | 16 + storybook/views/use-case.tsx | 69 + test/i18n.test.ts | 62 + test/mock-async-storage.ts | 3 + test/mock-file.ts | 6 + test/mock-i18n.ts | 5 + test/mock-react-native-image.ts | 20 + test/mock-reactotron.ts | 1 + test/setup.ts | 13 + test/storyshots.test.ts | 6 + tsconfig.json | 31 + webpack.config.js | 21 + yarn.lock | 19501 +++++++++++++++++++ 150 files changed, 24789 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .solidarity create mode 100644 App.js create mode 100644 README.md create mode 100644 app.json create mode 100644 app/app.tsx create mode 100644 app/components/auto-image/auto-image.story.tsx create mode 100644 app/components/auto-image/auto-image.tsx create mode 100644 app/components/bullet-item/bullet-item.tsx create mode 100644 app/components/button/button.presets.ts create mode 100644 app/components/button/button.props.ts create mode 100644 app/components/button/button.story.tsx create mode 100644 app/components/button/button.tsx create mode 100644 app/components/checkbox/checkbox.props.ts create mode 100644 app/components/checkbox/checkbox.story.tsx create mode 100644 app/components/checkbox/checkbox.tsx create mode 100644 app/components/form-row/form-row.presets.ts create mode 100644 app/components/form-row/form-row.props.tsx create mode 100644 app/components/form-row/form-row.story.tsx create mode 100644 app/components/form-row/form-row.tsx create mode 100644 app/components/header/header.props.ts create mode 100644 app/components/header/header.story.tsx create mode 100644 app/components/header/header.tsx create mode 100644 app/components/icon/icon.props.ts create mode 100644 app/components/icon/icon.story.tsx create mode 100644 app/components/icon/icon.tsx create mode 100644 app/components/icon/icons/arrow-left.png create mode 100644 app/components/icon/icons/arrow-left@2x.png create mode 100644 app/components/icon/icons/bullet.png create mode 100644 app/components/icon/icons/bullet@2x.png create mode 100644 app/components/icon/icons/index.ts create mode 100644 app/components/index.ts create mode 100644 app/components/screen/screen.presets.ts create mode 100644 app/components/screen/screen.props.ts create mode 100644 app/components/screen/screen.tsx create mode 100644 app/components/switch/switch.props.ts create mode 100644 app/components/switch/switch.story.tsx create mode 100644 app/components/switch/switch.tsx create mode 100644 app/components/text-field/text-field.story.tsx create mode 100644 app/components/text-field/text-field.tsx create mode 100644 app/components/text/text.presets.ts create mode 100644 app/components/text/text.props.ts create mode 100644 app/components/text/text.story.tsx create mode 100644 app/components/text/text.tsx create mode 100644 app/components/wallpaper/bg.png create mode 100644 app/components/wallpaper/bg@2x.png create mode 100644 app/components/wallpaper/wallpaper.presets.ts create mode 100644 app/components/wallpaper/wallpaper.props.ts create mode 100644 app/components/wallpaper/wallpaper.story.tsx create mode 100644 app/components/wallpaper/wallpaper.tsx create mode 100644 app/config/env.js create mode 100644 app/i18n/en.json create mode 100644 app/i18n/i18n.ts create mode 100644 app/i18n/index.ts create mode 100644 app/i18n/ja.json create mode 100644 app/i18n/translate.ts create mode 100644 app/models/character-store/character-store.test.ts create mode 100644 app/models/character-store/character-store.ts create mode 100644 app/models/character/character.test.ts create mode 100644 app/models/character/character.ts create mode 100644 app/models/environment.ts create mode 100644 app/models/extensions/with-environment.ts create mode 100644 app/models/extensions/with-root-store.ts create mode 100644 app/models/index.ts create mode 100644 app/models/root-store/root-store-context.ts create mode 100644 app/models/root-store/root-store.ts create mode 100644 app/models/root-store/setup-root-store.ts create mode 100644 app/navigators/index.ts create mode 100644 app/navigators/main-navigator.tsx create mode 100644 app/navigators/navigation-utilities.tsx create mode 100644 app/navigators/root-navigator.tsx create mode 100644 app/screens/demo/demo-list-screen.tsx create mode 100644 app/screens/demo/demo-screen.tsx create mode 100644 app/screens/demo/heart.png create mode 100644 app/screens/demo/heart@2x.png create mode 100644 app/screens/demo/logo-ignite.png create mode 100644 app/screens/demo/logo-ignite@2x.png create mode 100644 app/screens/index.ts create mode 100644 app/screens/welcome/bowser.png create mode 100644 app/screens/welcome/bowser@2x.png create mode 100644 app/screens/welcome/welcome-screen.tsx create mode 100644 app/services/api/api-config.ts create mode 100644 app/services/api/api-problem.test.ts create mode 100644 app/services/api/api-problem.ts create mode 100644 app/services/api/api.ts create mode 100644 app/services/api/api.types.ts create mode 100644 app/services/api/character-api.ts create mode 100644 app/services/api/index.ts create mode 100644 app/services/reactotron/index.ts create mode 100644 app/services/reactotron/reactotron-config.ts create mode 100644 app/services/reactotron/reactotron.ts create mode 100644 app/services/reactotron/tron.ts create mode 100644 app/services/reactotron/tron.web.ts create mode 100644 app/theme/color.ts create mode 100644 app/theme/fonts/index.ts create mode 100644 app/theme/index.ts create mode 100644 app/theme/palette.ts create mode 100644 app/theme/spacing.ts create mode 100644 app/theme/timing.ts create mode 100644 app/theme/typography.ts create mode 100644 app/utils/delay.ts create mode 100644 app/utils/ignore-warnings.ts create mode 100644 app/utils/keychain.ts create mode 100644 app/utils/storage/index.ts create mode 100644 app/utils/storage/storage.test.ts create mode 100644 app/utils/storage/storage.ts create mode 100644 app/utils/validate.ts create mode 100644 assets/fonts/custom-fonts.md create mode 100644 assets/images/adaptive-icon.png create mode 100644 assets/images/favicon.png create mode 100644 assets/images/icon.png create mode 100644 assets/images/splash.png create mode 100644 babel.config.js create mode 100755 bin/downloadExpoApp.sh create mode 100755 bin/postInstall create mode 100755 bin/setup create mode 100644 e2e/README.md create mode 100644 e2e/config.json create mode 100644 e2e/firstTest.spec.js create mode 100644 e2e/init.js create mode 100644 e2e/reload.js create mode 100644 ignite/templates/component/NAME.story.tsx.ejs create mode 100644 ignite/templates/component/NAME.tsx.ejs create mode 100644 ignite/templates/model/NAME.test.ts.ejs create mode 100644 ignite/templates/model/NAME.ts.ejs create mode 100644 ignite/templates/navigator/NAME-navigator.tsx.ejs create mode 100644 ignite/templates/screen/NAME-screen.tsx.ejs create mode 100644 package.json create mode 100644 react-native.config.js create mode 100644 storybook/index.ts create mode 100644 storybook/storybook-registry.ts create mode 100644 storybook/storybook.tsx create mode 100644 storybook/toggle-storybook.tsx create mode 100644 storybook/toggle-storybook.web.tsx create mode 100644 storybook/views/index.ts create mode 100644 storybook/views/story-screen.tsx create mode 100644 storybook/views/story.tsx create mode 100644 storybook/views/use-case.tsx create mode 100644 test/i18n.test.ts create mode 100644 test/mock-async-storage.ts create mode 100644 test/mock-file.ts create mode 100644 test/mock-i18n.ts create mode 100644 test/mock-react-native-image.ts create mode 100644 test/mock-reactotron.ts create mode 100644 test/setup.ts create mode 100644 test/storyshots.test.ts create mode 100644 tsconfig.json create mode 100644 webpack.config.js create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..282a81d --- /dev/null +++ b/.gitignore @@ -0,0 +1,83 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +*.keystore +!debug.keystore + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +*/fastlane/report.xml +*/fastlane/Preview.html +*/fastlane/screenshots + +# Bundle artifact +*.jsbundle + +# CocoaPods +/ios/Pods/ + +# Ignite-specific items below +# You can safely replace everything above this comment with whatever is +# in the default .gitignore generated by React-Native CLI + +# VS Code +.vscode + +# Expo +.expo/* +bin/Exponent.app + +npm-debug.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* +web-build/ + +# Configurations +app/config/env.*.js +!env.js diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..883e252 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +node_modules +ios +android +.vscode +ignite/ignite.json +package.json diff --git a/.solidarity b/.solidarity new file mode 100644 index 0000000..cd5e390 --- /dev/null +++ b/.solidarity @@ -0,0 +1,24 @@ +{ + "$schema": "http://json.schemastore.org/solidaritySchema", + "requirements": { + "Node": [{ "rule": "cli", "binary": "node", "semver": ">=8.6.0" }], + "Xcode": [ + { + "rule": "cli", + "binary": "xcodebuild", + "version": "-version", + "semver": ">=9.2.0", + "platform": "darwin" + } + ], + "CocoaPods": [ + { + "rule": "cli", + "binary": "pod", + "version": "--version", + "semver": ">=1.7.0", + "platform": "darwin" + } + ] + } +} diff --git a/App.js b/App.js new file mode 100644 index 0000000..9adab2e --- /dev/null +++ b/App.js @@ -0,0 +1,6 @@ +// This is the first file that ReactNative will run when it starts up. +import App from "./app/app.tsx" +import { registerRootComponent } from "expo" + +registerRootComponent(App) +export default App diff --git a/README.md b/README.md new file mode 100644 index 0000000..aaeb18c --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ +# Welcome to your new ignited app! + +[![CircleCI](https://circleci.com/gh/infinitered/ignite.svg?style=svg)](https://circleci.com/gh/infinitered/ignite) + +## The latest and greatest boilerplate for Infinite Red opinions + +This is the boilerplate that [Infinite Red](https://infinite.red) uses as a way to test bleeding-edge changes to our React Native stack. + +Currently includes: + +- React Native +- React Navigation +- MobX State Tree +- TypeScript +- And more! + +## Quick Start + +The Ignite boilerplate project's structure will look similar to this: + +``` +ignite-project +├── app +│   ├── components +│   ├── i18n +│   ├── utils +│   ├── models +│   ├── navigators +│   ├── screens +│   ├── services +│   ├── theme +│   ├── app.tsx +├── storybook +│   ├── views +│   ├── index.ts +│   ├── storybook-registry.ts +│   ├── storybook.ts +│   ├── toggle-storybook.tsx +├── test +│   ├── __snapshots__ +│   ├── storyshots.test.ts.snap +│   ├── mock-i18n.ts +│   ├── mock-reactotron.ts +│   ├── setup.ts +│   ├── storyshots.test.ts +├── README.md +├── android +│   ├── app +│   ├── build.gradle +│   ├── gradle +│   ├── gradle.properties +│   ├── gradlew +│   ├── gradlew.bat +│   ├── keystores +│   └── settings.gradle +├── ignite +│   ├── ignite.json +│   └── plugins +├── index.js +├── ios +│   ├── IgniteProject +│   ├── IgniteProject-tvOS +│   ├── IgniteProject-tvOSTests +│   ├── IgniteProject.xcodeproj +│   └── IgniteProjectTests +├── .env +└── package.json + +``` + +### ./app directory + +Included in an Ignite boilerplate project is the `app` directory. This is a directory you would normally have to create when using vanilla React Native. + +The inside of the src directory looks similar to the following: + +``` +app +│── components +│── i18n +├── models +├── navigators +├── screens +├── services +├── theme +├── utils +└── app.tsx +``` + +**components** +This is where your React components will live. Each component will have a directory containing the `.tsx` file, along with a story file, and optionally `.presets`, and `.props` files for larger components. The app will come with some commonly used components like Button. + +**i18n** +This is where your translations will live if you are using `react-native-i18n`. + +**models** +This is where your app's models will live. Each model has a directory which will contain the `mobx-state-tree` model file, test file, and any other supporting files like actions, types, etc. + +**navigators** +This is where your `react-navigation` navigators will live. + +**screens** +This is where your screen components will live. A screen is a React component which will take up the entire screen and be part of the navigation hierarchy. Each screen will have a directory containing the `.tsx` file, along with any assets or other helper files. + +**services** +Any services that interface with the outside world will live here (think REST APIs, Push Notifications, etc.). + +**theme** +Here lives the theme for your application, including spacing, colors, and typography. + +**utils** +This is a great place to put miscellaneous helpers and utilities. Things like date helpers, formatters, etc. are often found here. However, it should only be used for things that are truely shared across your application. If a helper or utility is only used by a specific component or model, consider co-locating your helper with that component or model. + +**app.tsx** This is the entry point to your app. This is where you will find the main App component which renders the rest of the application. + +### ./ignite directory + +The `ignite` directory stores all things Ignite, including CLI and boilerplate items. Here you will find generators, plugins and examples to help you get started with React Native. + +### ./storybook directory + +This is where your stories will be registered and where the Storybook configs will live. + +### ./test directory + +This directory will hold your Jest configs and mocks, as well as your [storyshots](https://github.com/storybooks/storybook/tree/master/addons/storyshots) test file. This is a file that contains the snapshots of all your component storybooks. + +## Running Storybook + +From the command line in your generated app's root directory, enter `yarn run storybook` +This starts up the storybook server and opens a story navigator in your browser. With your app +running, choose Toggle Storybook from the developer menu to switch to Storybook; you can then +use the story navigator in your browser to change stories. + +For Visual Studio Code users, there is a handy extension that makes it easy to load Storybook use cases into a running emulator via tapping on items in the editor sidebar. Install the `React Native Storybook` extension by `Orta`, hit `cmd + shift + P` and select "Reconnect Storybook to VSCode". Expand the STORYBOOK section in the sidebar to see all use cases for components that have `.story.tsx` files in their directories. + +## Running e2e tests + +Read [e2e setup instructions](./e2e/README.md). + +## Previous Boilerplates + +- [2018 aka Bowser](https://github.com/infinitered/ignite-bowser) +- [2017 aka Andross](https://github.com/infinitered/ignite-andross) +- [2016 aka Ignite 1.0](https://github.com/infinitered/ignite-ir-boilerplate-2016) diff --git a/app.json b/app.json new file mode 100644 index 0000000..167a1ae --- /dev/null +++ b/app.json @@ -0,0 +1,26 @@ +{ + "expo": { + "name": "OrgRoamForceGraphReact", + "slug": "OrgRoamForceGraphReact", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "splash": { + "image": "./assets/images/splash.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "updates": { + "fallbackToCacheTimeout": 0 + }, + "assetBundlePatterns": [ + "**/*" + ], + "ios": { + "supportsTablet": true + }, + "web": { + "favicon": "./assets/images/favicon.png" + } + } +} diff --git a/app/app.tsx b/app/app.tsx new file mode 100644 index 0000000..d737864 --- /dev/null +++ b/app/app.tsx @@ -0,0 +1,81 @@ +/** + * Welcome to the main entry point of the app. In this file, we'll + * be kicking off our app. + * + * Most of this file is boilerplate and you shouldn't need to modify + * it very often. But take some time to look through and understand + * what is going on here. + * + * The app navigation resides in ./app/navigators, so head over there + * if you're interested in adding screens and navigators. + */ +import "./i18n" +import "./utils/ignore-warnings" +import React, { useState, useEffect, useRef } from "react" +import { NavigationContainerRef } from "@react-navigation/native" +import { SafeAreaProvider, initialWindowMetrics } from "react-native-safe-area-context" +import { initFonts } from "./theme/fonts" // expo +import * as storage from "./utils/storage" +import { + useBackButtonHandler, + RootNavigator, + canExit, + setRootNavigation, + useNavigationPersistence, +} from "./navigators" +import { RootStore, RootStoreProvider, setupRootStore } from "./models" +import { ToggleStorybook } from "../storybook/toggle-storybook" + +// This puts screens in a native ViewController or Activity. If you want fully native +// stack navigation, use `createNativeStackNavigator` in place of `createStackNavigator`: +// https://github.com/kmagiera/react-native-screens#using-native-stack-navigator +import { enableScreens } from "react-native-screens" +enableScreens() + +export const NAVIGATION_PERSISTENCE_KEY = "NAVIGATION_STATE" + +/** + * This is the root component of our app. + */ +function App() { + const navigationRef = useRef(null) + const [rootStore, setRootStore] = useState(undefined) + + setRootNavigation(navigationRef) + useBackButtonHandler(navigationRef, canExit) + const { initialNavigationState, onNavigationStateChange } = useNavigationPersistence( + storage, + NAVIGATION_PERSISTENCE_KEY, + ) + + // Kick off initial async loading actions, like loading fonts and RootStore + useEffect(() => { + ;(async () => { + await initFonts() // expo + setupRootStore().then(setRootStore) + })() + }, []) + + // Before we show the app, we have to wait for our state to be ready. + // In the meantime, don't render anything. This will be the background + // color set in native by rootView's background color. You can replace + // with your own loading component if you wish. + if (!rootStore) return null + + // otherwise, we're ready to render the app + return ( + + + + + + + + ) +} + +export default App diff --git a/app/components/auto-image/auto-image.story.tsx b/app/components/auto-image/auto-image.story.tsx new file mode 100644 index 0000000..f7ecc86 --- /dev/null +++ b/app/components/auto-image/auto-image.story.tsx @@ -0,0 +1,31 @@ +/* eslint-disable */ +import * as React from "react" +import { storiesOf } from "@storybook/react-native" +import { StoryScreen, Story, UseCase } from "../../../storybook/views" +import { AutoImage } from "./auto-image" + +declare let module + +const bowser = require("../../screens/welcome/bowser.png") +const morty = { uri: "https://rickandmortyapi.com/api/character/avatar/2.jpeg" } + +storiesOf("AutoImage", module) + .addDecorator((fn) => {fn()}) + .add("Style Presets", () => ( + + + + + + + + + + + + + + + + + )) diff --git a/app/components/auto-image/auto-image.tsx b/app/components/auto-image/auto-image.tsx new file mode 100644 index 0000000..39d71ca --- /dev/null +++ b/app/components/auto-image/auto-image.tsx @@ -0,0 +1,46 @@ +import React, { useLayoutEffect, useState } from "react" +import { + Image as RNImage, + ImageProps as DefaultImageProps, + ImageURISource, + Platform, +} from "react-native" + +type ImageProps = DefaultImageProps & { + source: ImageURISource +} + +/** + * An Image wrapper component that autosizes itself to the size of the actual image. + * You can always override by passing a width and height in the style. + * If passing only one of width/height this image component will use the actual + * size of the other dimension. + * + * This component isn't required, but is provided as a convenience so that + * we don't have to remember to explicitly set image sizes on every image instance. + * + * To use as a stand-in replacement import { AutoImage as Image } and remove the + * Image import from react-native. Now all images in that file are handled by this + * component and are web-ready if not explicitly sized in the style property. + */ +export function AutoImage(props: ImageProps) { + const [imageSize, setImageSize] = useState({ width: 0, height: 0 }) + + useLayoutEffect(() => { + if (props.source?.uri) { + RNImage.getSize(props.source.uri as any, (width, height) => { + setImageSize({ width, height }) + }) + } else if (Platform.OS === "web") { + // web requires a different method to get it's size + RNImage.getSize(props.source as any, (width, height) => { + setImageSize({ width, height }) + }) + } else { + const { width, height } = RNImage.resolveAssetSource(props.source) + setImageSize({ width, height }) + } + }, []) + + return +} diff --git a/app/components/bullet-item/bullet-item.tsx b/app/components/bullet-item/bullet-item.tsx new file mode 100644 index 0000000..d999e10 --- /dev/null +++ b/app/components/bullet-item/bullet-item.tsx @@ -0,0 +1,41 @@ +import * as React from "react" +import { View, ViewStyle, ImageStyle, TextStyle } from "react-native" +import { Text } from "../text/text" +import { Icon } from "../icon/icon" +import { spacing, typography } from "../../theme" + +const BULLET_ITEM: ViewStyle = { + flexDirection: "row", + marginTop: spacing[4], + paddingBottom: spacing[4], + borderBottomWidth: 1, + borderBottomColor: "#3A3048", +} +const BULLET_CONTAINER: ViewStyle = { + marginRight: spacing[4] - 1, + marginTop: spacing[2], +} +const BULLET: ImageStyle = { + width: 8, + height: 8, +} +const BULLET_TEXT: TextStyle = { + flex: 1, + fontFamily: typography.primary, + color: "#BAB6C8", + fontSize: 15, + lineHeight: 22, +} + +export interface BulletItemProps { + text: string +} + +export function BulletItem(props: BulletItemProps) { + return ( + + + + + ) +} diff --git a/app/components/button/button.presets.ts b/app/components/button/button.presets.ts new file mode 100644 index 0000000..b140fd2 --- /dev/null +++ b/app/components/button/button.presets.ts @@ -0,0 +1,54 @@ +import { ViewStyle, TextStyle } from "react-native" +import { color, spacing } from "../../theme" + +/** + * All text will start off looking like this. + */ +const BASE_VIEW: ViewStyle = { + paddingVertical: spacing[2], + paddingHorizontal: spacing[2], + borderRadius: 4, + justifyContent: "center", + alignItems: "center", +} + +const BASE_TEXT: TextStyle = { + paddingHorizontal: spacing[3], +} + +/** + * All the variations of text styling within the app. + * + * You want to customize these to whatever you need in your app. + */ +export const viewPresets: Record = { + /** + * A smaller piece of secondard information. + */ + primary: { ...BASE_VIEW, backgroundColor: color.palette.orange } as ViewStyle, + + /** + * A button without extras. + */ + link: { + ...BASE_VIEW, + paddingHorizontal: 0, + paddingVertical: 0, + alignItems: "flex-start", + } as ViewStyle, +} + +export const textPresets: Record = { + primary: { ...BASE_TEXT, fontSize: 9, color: color.palette.white } as TextStyle, + link: { + ...BASE_TEXT, + color: color.text, + paddingHorizontal: 0, + paddingVertical: 0, + } as TextStyle, +} + +/** + * A list of preset names. + */ +export type ButtonPresetNames = keyof typeof viewPresets diff --git a/app/components/button/button.props.ts b/app/components/button/button.props.ts new file mode 100644 index 0000000..1377a7e --- /dev/null +++ b/app/components/button/button.props.ts @@ -0,0 +1,35 @@ +import { StyleProp, TextStyle, TouchableOpacityProps, ViewStyle } from "react-native" +import { ButtonPresetNames } from "./button.presets" +import { TxKeyPath } from "../../i18n" + +export interface ButtonProps extends TouchableOpacityProps { + /** + * Text which is looked up via i18n. + */ + tx?: TxKeyPath + + /** + * The text to display if not using `tx` or nested components. + */ + text?: string + + /** + * An optional style override useful for padding & margin. + */ + style?: StyleProp + + /** + * An optional style override useful for the button text. + */ + textStyle?: StyleProp + + /** + * One of the different types of text presets. + */ + preset?: ButtonPresetNames + + /** + * One of the different types of text presets. + */ + children?: React.ReactNode +} diff --git a/app/components/button/button.story.tsx b/app/components/button/button.story.tsx new file mode 100644 index 0000000..4861772 --- /dev/null +++ b/app/components/button/button.story.tsx @@ -0,0 +1,33 @@ +import * as React from "react" +import { ViewStyle, TextStyle, Alert } from "react-native" +import { storiesOf } from "@storybook/react-native" +import { StoryScreen, Story, UseCase } from "../../../storybook/views" +import { Button } from "./button" + +declare let module + +const buttonStyleArray: ViewStyle[] = [{ paddingVertical: 100 }, { borderRadius: 0 }] + +const buttonTextStyleArray: TextStyle[] = [{ fontSize: 20 }, { color: "#a511dc" }] + +storiesOf("Button", module) + .addDecorator((fn) => {fn()}) + .add("Style Presets", () => ( + + + + ) : ( + + )} + + + + {rightIcon ? ( + + ) : ( + + )} + + ) +} diff --git a/app/components/icon/icon.props.ts b/app/components/icon/icon.props.ts new file mode 100644 index 0000000..71ce0b7 --- /dev/null +++ b/app/components/icon/icon.props.ts @@ -0,0 +1,21 @@ +import { ImageStyle, StyleProp, ViewStyle } from "react-native" +import { IconTypes } from "./icons" + +export interface IconProps { + /** + * Style overrides for the icon image + */ + style?: StyleProp + + /** + * Style overrides for the icon container + */ + + containerStyle?: StyleProp + + /** + * The name of the icon + */ + + icon?: IconTypes +} diff --git a/app/components/icon/icon.story.tsx b/app/components/icon/icon.story.tsx new file mode 100644 index 0000000..d119ed4 --- /dev/null +++ b/app/components/icon/icon.story.tsx @@ -0,0 +1,19 @@ +import * as React from "react" +import { storiesOf } from "@storybook/react-native" +import { StoryScreen, Story, UseCase } from "../../../storybook/views" +import { Icon } from "./icon" + +declare let module + +storiesOf("Icon", module) + .addDecorator((fn) => {fn()}) + .add("Names", () => ( + + + + + + + + + )) diff --git a/app/components/icon/icon.tsx b/app/components/icon/icon.tsx new file mode 100644 index 0000000..4735e13 --- /dev/null +++ b/app/components/icon/icon.tsx @@ -0,0 +1,19 @@ +import * as React from "react" +import { View, ImageStyle } from "react-native" +import { AutoImage as Image } from "../auto-image/auto-image" +import { IconProps } from "./icon.props" +import { icons } from "./icons" + +const ROOT: ImageStyle = { + resizeMode: "contain", +} + +export function Icon(props: IconProps) { + const { style: styleOverride, icon, containerStyle } = props + + return ( + + + + ) +} diff --git a/app/components/icon/icons/arrow-left.png b/app/components/icon/icons/arrow-left.png new file mode 100644 index 0000000..9d607d7 Binary files /dev/null and b/app/components/icon/icons/arrow-left.png differ diff --git a/app/components/icon/icons/arrow-left@2x.png b/app/components/icon/icons/arrow-left@2x.png new file mode 100644 index 0000000..9d607d7 Binary files /dev/null and b/app/components/icon/icons/arrow-left@2x.png differ diff --git a/app/components/icon/icons/bullet.png b/app/components/icon/icons/bullet.png new file mode 100644 index 0000000..8fc256f Binary files /dev/null and b/app/components/icon/icons/bullet.png differ diff --git a/app/components/icon/icons/bullet@2x.png b/app/components/icon/icons/bullet@2x.png new file mode 100644 index 0000000..8fc256f Binary files /dev/null and b/app/components/icon/icons/bullet@2x.png differ diff --git a/app/components/icon/icons/index.ts b/app/components/icon/icons/index.ts new file mode 100644 index 0000000..00e8a59 --- /dev/null +++ b/app/components/icon/icons/index.ts @@ -0,0 +1,6 @@ +export const icons = { + back: require("./arrow-left.png"), + bullet: require("./bullet.png"), +} + +export type IconTypes = keyof typeof icons diff --git a/app/components/index.ts b/app/components/index.ts new file mode 100644 index 0000000..5ed4a28 --- /dev/null +++ b/app/components/index.ts @@ -0,0 +1,12 @@ +export * from "./bullet-item/bullet-item" +export * from "./button/button" +export * from "./checkbox/checkbox" +export * from "./form-row/form-row" +export * from "./header/header" +export * from "./icon/icon" +export * from "./screen/screen" +export * from "./switch/switch" +export * from "./text/text" +export * from "./text-field/text-field" +export * from "./wallpaper/wallpaper" +export * from "./auto-image/auto-image" diff --git a/app/components/screen/screen.presets.ts b/app/components/screen/screen.presets.ts new file mode 100644 index 0000000..a016b77 --- /dev/null +++ b/app/components/screen/screen.presets.ts @@ -0,0 +1,66 @@ +import { ViewStyle } from "react-native" +import { color } from "../../theme" + +/** + * All screen keyboard offsets. + */ +export const offsets = { + none: 0, +} + +/** + * The variations of keyboard offsets. + */ +export type KeyboardOffsets = keyof typeof offsets + +/** + * All the variations of screens. + */ +export const presets = { + /** + * No scrolling. Suitable for full-screen carousels and components + * which have built-in scrolling like FlatList. + */ + fixed: { + outer: { + backgroundColor: color.background, + flex: 1, + height: "100%", + } as ViewStyle, + inner: { + justifyContent: "flex-start", + alignItems: "stretch", + height: "100%", + width: "100%", + } as ViewStyle, + }, + + /** + * Scrolls. Suitable for forms or other things requiring a keyboard. + * + * Pick this one if you don't know which one you want yet. + */ + scroll: { + outer: { + backgroundColor: color.background, + flex: 1, + height: "100%", + } as ViewStyle, + inner: { justifyContent: "flex-start", alignItems: "stretch" } as ViewStyle, + }, +} + +/** + * The variations of screens. + */ +export type ScreenPresets = keyof typeof presets + +/** + * Is this preset a non-scrolling one? + * + * @param preset The preset to check + */ +export function isNonScrolling(preset?: ScreenPresets) { + // any of these things will make you scroll + return !preset || !presets[preset] || preset === "fixed" +} diff --git a/app/components/screen/screen.props.ts b/app/components/screen/screen.props.ts new file mode 100644 index 0000000..0326fd7 --- /dev/null +++ b/app/components/screen/screen.props.ts @@ -0,0 +1,46 @@ +import React from "react" +import { StyleProp, ViewStyle } from "react-native" +import { KeyboardOffsets, ScreenPresets } from "./screen.presets" + +export interface ScreenProps { + /** + * Children components. + */ + children?: React.ReactNode + + /** + * An optional style override useful for padding & margin. + */ + style?: StyleProp + + /** + * One of the different types of presets. + */ + preset?: ScreenPresets + + /** + * An optional background color + */ + backgroundColor?: string + + /** + * An optional status bar setting. Defaults to light-content. + */ + statusBar?: "light-content" | "dark-content" + + /** + * Should we not wrap in SafeAreaView? Defaults to false. + */ + unsafe?: boolean + + /** + * By how much should we offset the keyboard? Defaults to none. + */ + keyboardOffset?: KeyboardOffsets + + /** + * Should keyboard persist on screen tap. Defaults to handled. + * Only applies to scroll preset. + */ + keyboardShouldPersistTaps?: "handled" | "always" | "never" +} diff --git a/app/components/screen/screen.tsx b/app/components/screen/screen.tsx new file mode 100644 index 0000000..ba84547 --- /dev/null +++ b/app/components/screen/screen.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { KeyboardAvoidingView, Platform, ScrollView, StatusBar, View } from "react-native" +import { useSafeAreaInsets } from "react-native-safe-area-context" +import { ScreenProps } from "./screen.props" +import { isNonScrolling, offsets, presets } from "./screen.presets" + +const isIos = Platform.OS === "ios" + +function ScreenWithoutScrolling(props: ScreenProps) { + const insets = useSafeAreaInsets() + const preset = presets.fixed + const style = props.style || {} + const backgroundStyle = props.backgroundColor ? { backgroundColor: props.backgroundColor } : {} + const insetStyle = { paddingTop: props.unsafe ? 0 : insets.top } + + return ( + + + {props.children} + + ) +} + +function ScreenWithScrolling(props: ScreenProps) { + const insets = useSafeAreaInsets() + const preset = presets.scroll + const style = props.style || {} + const backgroundStyle = props.backgroundColor ? { backgroundColor: props.backgroundColor } : {} + const insetStyle = { paddingTop: props.unsafe ? 0 : insets.top } + + return ( + + + + + {props.children} + + + + ) +} + +/** + * The starting component on every screen in the app. + * + * @param props The screen props + */ +export function Screen(props: ScreenProps) { + if (isNonScrolling(props.preset)) { + return + } else { + return + } +} diff --git a/app/components/switch/switch.props.ts b/app/components/switch/switch.props.ts new file mode 100644 index 0000000..8235457 --- /dev/null +++ b/app/components/switch/switch.props.ts @@ -0,0 +1,39 @@ +import { StyleProp, ViewStyle } from "react-native" + +export interface SwitchProps { + /** + * On or off. + */ + value?: boolean + /** + * Fires when the on/off switch triggers. + * + * @param newValue The new value we're switching to. + */ + onToggle?: (newValue: boolean) => void + + /** + * A style override to apply to the container. Useful for margins and paddings. + */ + style?: StyleProp + + /** + * Additional track styling when on. + */ + trackOnStyle?: StyleProp + + /** + * Additional track styling when off. + */ + trackOffStyle?: StyleProp + + /** + * Additional thumb styling when on. + */ + thumbOnStyle?: StyleProp + + /** + * Additional thumb styling when off. + */ + thumbOffStyle?: StyleProp +} diff --git a/app/components/switch/switch.story.tsx b/app/components/switch/switch.story.tsx new file mode 100644 index 0000000..998d1df --- /dev/null +++ b/app/components/switch/switch.story.tsx @@ -0,0 +1,110 @@ +/* eslint-disable react-native/no-inline-styles */ +/* eslint-disable react-native/no-color-literals */ + +import * as React from "react" +import { View, ViewStyle } from "react-native" +import { storiesOf } from "@storybook/react-native" +import { StoryScreen, Story, UseCase } from "../../../storybook/views" +import { Toggle } from "react-powerplug" +import { Switch } from "./switch" + +declare let module + +const styleArray: ViewStyle[] = [{ borderColor: "#686868" }] + +const trackOffStyle: ViewStyle[] = [ + { backgroundColor: "#686868" }, + { + height: 80, + borderRadius: 0, + }, +] +const trackOnStyle: ViewStyle[] = [ + { + backgroundColor: "#b1008e", + borderColor: "#686868", + }, + { + height: 80, + borderRadius: 0, + }, +] +const thumbOffStyle: ViewStyle[] = [ + { + backgroundColor: "#b1008e", + borderColor: "#686868", + }, + { + height: 80, + borderRadius: 0, + }, +] +const thumbOnStyle: ViewStyle[] = [ + { backgroundColor: "#f0c" }, + { + height: 80, + borderRadius: 0, + borderColor: "#686868", + }, +] + +storiesOf("Switch", module) + .addDecorator((fn) => {fn()}) + .add("Behaviour", () => ( + + + + {({ on, toggle }) => } + + + + + + + + + + )) + .add("Styling", () => ( + + + + {({ on, toggle }) => ( + + + + )} + + + + + + {({ on, toggle }) => ( + + + + )} + + + + )) diff --git a/app/components/switch/switch.tsx b/app/components/switch/switch.tsx new file mode 100644 index 0000000..0813747 --- /dev/null +++ b/app/components/switch/switch.tsx @@ -0,0 +1,114 @@ +import React from "react" +import { ViewStyle, Animated, Easing, TouchableWithoutFeedback } from "react-native" +import { color } from "../../theme" +import { SwitchProps } from "./switch.props" + +// dimensions +const THUMB_SIZE = 30 +const WIDTH = 56 +const MARGIN = 2 +const OFF_POSITION = -0.5 +const ON_POSITION = WIDTH - THUMB_SIZE - MARGIN +const BORDER_RADIUS = (THUMB_SIZE * 3) / 4 + +// colors +const ON_COLOR = color.primary +const OFF_COLOR = color.palette.offWhite +const BORDER_ON_COLOR = ON_COLOR +const BORDER_OFF_COLOR = "rgba(0, 0, 0, 0.1)" + +// animation +const DURATION = 250 + +// the track always has these props +const TRACK = { + height: THUMB_SIZE + MARGIN, + width: WIDTH, + borderRadius: BORDER_RADIUS, + borderWidth: MARGIN / 2, + backgroundColor: color.background, +} + +// the thumb always has these props +const THUMB: ViewStyle = { + position: "absolute", + width: THUMB_SIZE, + height: THUMB_SIZE, + borderColor: BORDER_OFF_COLOR, + borderRadius: THUMB_SIZE / 2, + borderWidth: MARGIN / 2, + backgroundColor: color.background, + shadowColor: BORDER_OFF_COLOR, + shadowOffset: { width: 1, height: 2 }, + shadowOpacity: 1, + shadowRadius: 2, + elevation: 2, +} + +const makeAnimatedValue = (switchOn) => new Animated.Value(switchOn ? 1 : 0) + +export function Switch(props: SwitchProps) { + const [timer] = React.useState(makeAnimatedValue(props.value)) + const startAnimation = React.useMemo( + () => (newValue: boolean) => { + const toValue = newValue ? 1 : 0 + const easing = Easing.out(Easing.circle) + Animated.timing(timer, { + toValue, + duration: DURATION, + easing, + useNativeDriver: true, + }).start() + }, + [timer], + ) + + const [previousValue, setPreviousValue] = React.useState(props.value) + React.useEffect(() => { + if (props.value !== previousValue) { + startAnimation(props.value) + setPreviousValue(props.value) + } + }, [props.value]) + + const handlePress = React.useMemo(() => () => props.onToggle && props.onToggle(!props.value), [ + props.onToggle, + props.value, + ]) + + if (!timer) { + return null + } + + const translateX = timer.interpolate({ + inputRange: [0, 1], + outputRange: [OFF_POSITION, ON_POSITION], + }) + + const style = props.style + + const trackStyle = [ + TRACK, + { + backgroundColor: props.value ? ON_COLOR : OFF_COLOR, + borderColor: props.value ? BORDER_ON_COLOR : BORDER_OFF_COLOR, + }, + props.value ? props.trackOnStyle : props.trackOffStyle, + ] + + const thumbStyle = [ + THUMB, + { + transform: [{ translateX }], + }, + props.value ? props.thumbOnStyle : props.thumbOffStyle, + ] + + return ( + + + + + + ) +} diff --git a/app/components/text-field/text-field.story.tsx b/app/components/text-field/text-field.story.tsx new file mode 100644 index 0000000..74a4da0 --- /dev/null +++ b/app/components/text-field/text-field.story.tsx @@ -0,0 +1,159 @@ +/* eslint-disable react-native/no-inline-styles */ +/* eslint-disable react-native/no-color-literals */ + +import * as React from "react" +import { storiesOf } from "@storybook/react-native" +import { StoryScreen, Story, UseCase } from "../../../storybook/views" +import { Text, TextField } from "../" +import { State } from "react-powerplug" +import { ViewStyle, TextStyle, Alert } from "react-native" + +declare let module + +const styleArray: ViewStyle[] = [{ paddingHorizontal: 30 }, { borderWidth: 30 }] + +const inputStyleArray: TextStyle[] = [ + { + backgroundColor: "rebeccapurple", + color: "white", + padding: 40, + }, + { + borderWidth: 10, + borderRadius: 4, + borderColor: "#7fff00", + }, +] +let alertWhenFocused = true + +storiesOf("TextField", module) + .addDecorator((fn) => {fn()}) + .add("Labelling", () => ( + + + + {({ state, setState }) => ( + setState({ value })} + value={state.value} + label="Name" + placeholder="omg your name" + /> + )} + + + + + + {({ state, setState }) => ( + setState({ value })} + value={state.value} + placeholderTx="storybook.placeholder" + labelTx="storybook.field" + /> + )} + + + + )) + .add("Style Overrides", () => ( + + + + {({ state, setState }) => ( + setState({ value })} + value={state.value} + label="First Name" + style={{ paddingTop: 0, paddingHorizontal: 40 }} + /> + )} + + + {({ state, setState }) => ( + setState({ value })} + value={state.value} + label="Last Name" + style={{ paddingBottom: 0 }} + /> + )} + + + + + {({ state, setState }) => ( + setState({ value })} + value={state.value} + label="Name" + inputStyle={{ + backgroundColor: "rebeccapurple", + color: "white", + padding: 40, + borderWidth: 10, + borderRadius: 4, + borderColor: "hotpink", + }} + /> + )} + + + + + + + {({ state, setState }) => ( + setState({ value })} + value={state.value} + label="Name" + style={styleArray} + inputStyle={inputStyleArray} + /> + )} + + + + + )) + .add("Ref Forwarding", () => ( + + + + {({ state, setState }) => ( + setState({ value })} + value={state.value} + label="Name" + inputStyle={{ + backgroundColor: "rebeccapurple", + color: "white", + padding: 40, + borderWidth: 10, + borderRadius: 4, + borderColor: "hotpink", + }} + forwardedRef={(ref) => ref} + onFocus={() => { + if (alertWhenFocused) { + // Prevent text field focus from being repeatedly triggering alert + alertWhenFocused = false + Alert.alert("Text field focuesed with forwarded ref!") + } + }} + /> + )} + + + + + )) diff --git a/app/components/text-field/text-field.tsx b/app/components/text-field/text-field.tsx new file mode 100644 index 0000000..eea1a70 --- /dev/null +++ b/app/components/text-field/text-field.tsx @@ -0,0 +1,98 @@ +import React from "react" +import { StyleProp, TextInput, TextInputProps, TextStyle, View, ViewStyle } from "react-native" +import { color, spacing, typography } from "../../theme" +import { translate, TxKeyPath } from "../../i18n" +import { Text } from "../text/text" + +// the base styling for the container +const CONTAINER: ViewStyle = { + paddingVertical: spacing[3], +} + +// the base styling for the TextInput +const INPUT: TextStyle = { + fontFamily: typography.primary, + color: color.text, + minHeight: 44, + fontSize: 18, + backgroundColor: color.palette.white, +} + +// currently we have no presets, but that changes quickly when you build your app. +const PRESETS: { [name: string]: ViewStyle } = { + default: {}, +} + +export interface TextFieldProps extends TextInputProps { + /** + * The placeholder i18n key. + */ + placeholderTx?: TxKeyPath + + /** + * The Placeholder text if no placeholderTx is provided. + */ + placeholder?: string + + /** + * The label i18n key. + */ + labelTx?: TxKeyPath + + /** + * The label text if no labelTx is provided. + */ + label?: string + + /** + * Optional container style overrides useful for margins & padding. + */ + style?: StyleProp + + /** + * Optional style overrides for the input. + */ + inputStyle?: StyleProp + + /** + * Various look & feels. + */ + preset?: keyof typeof PRESETS + + forwardedRef?: any +} + +/** + * A component which has a label and an input together. + */ +export function TextField(props: TextFieldProps) { + const { + placeholderTx, + placeholder, + labelTx, + label, + preset = "default", + style: styleOverride, + inputStyle: inputStyleOverride, + forwardedRef, + ...rest + } = props + + const containerStyles = [CONTAINER, PRESETS[preset], styleOverride] + const inputStyles = [INPUT, inputStyleOverride] + const actualPlaceholder = placeholderTx ? translate(placeholderTx) : placeholder + + return ( + + + + + ) +} diff --git a/app/components/text/text.presets.ts b/app/components/text/text.presets.ts new file mode 100644 index 0000000..9622268 --- /dev/null +++ b/app/components/text/text.presets.ts @@ -0,0 +1,48 @@ +import { TextStyle } from "react-native" +import { color, typography } from "../../theme" + +/** + * All text will start off looking like this. + */ +const BASE: TextStyle = { + fontFamily: typography.primary, + color: color.text, + fontSize: 15, +} + +/** + * All the variations of text styling within the app. + * + * You want to customize these to whatever you need in your app. + */ +export const presets = { + /** + * The default text styles. + */ + default: BASE, + + /** + * A bold version of the default text. + */ + bold: { ...BASE, fontWeight: "bold" } as TextStyle, + + /** + * Large headers. + */ + header: { ...BASE, fontSize: 24, fontWeight: "bold" } as TextStyle, + + /** + * Field labels that appear on forms above the inputs. + */ + fieldLabel: { ...BASE, fontSize: 13, color: color.dim } as TextStyle, + + /** + * A smaller piece of secondard information. + */ + secondary: { ...BASE, fontSize: 9, color: color.dim } as TextStyle, +} + +/** + * A list of preset names. + */ +export type TextPresets = keyof typeof presets diff --git a/app/components/text/text.props.ts b/app/components/text/text.props.ts new file mode 100644 index 0000000..d2c55dc --- /dev/null +++ b/app/components/text/text.props.ts @@ -0,0 +1,37 @@ +import { StyleProp, TextProps as TextProperties, TextStyle } from "react-native" +import i18n from "i18n-js" +import { TextPresets } from "./text.presets" +import { TxKeyPath } from "../../i18n" + +export interface TextProps extends TextProperties { + /** + * Children components. + */ + children?: React.ReactNode + + /** + * Text which is looked up via i18n. + */ + tx?: TxKeyPath + + /** + * Optional options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + txOptions?: i18n.TranslateOptions + + /** + * The text to display if not using `tx` or nested components. + */ + text?: string + + /** + * An optional style override useful for padding & margin. + */ + style?: StyleProp + + /** + * One of the different types of text presets. + */ + preset?: TextPresets +} diff --git a/app/components/text/text.story.tsx b/app/components/text/text.story.tsx new file mode 100644 index 0000000..5582c1b --- /dev/null +++ b/app/components/text/text.story.tsx @@ -0,0 +1,92 @@ +/* eslint-disable react-native/no-inline-styles */ +/* eslint-disable react-native/no-color-literals */ + +import * as React from "react" +import { View, ViewStyle } from "react-native" +import { storiesOf } from "@storybook/react-native" +import { StoryScreen, Story, UseCase } from "../../../storybook/views" +import { color } from "../../theme" +import { Text } from "./text" + +declare let module + +const VIEWSTYLE = { + flex: 1, + backgroundColor: color.storybookDarkBg, +} +const viewStyleArray: ViewStyle[] = [VIEWSTYLE, { backgroundColor: "#7fff00" }] + +storiesOf("Text", module) + .addDecorator((fn) => {fn()}) + .add("Style Presets", () => ( + + + + Hello! + + Check out{"\n"} + my{"\n"} + line height + + The quick brown fox jumped over the slow lazy dog. + $123,456,789.00 + + + + + Osnap! I'm puffy. + + + + + Behold! + + + + )) + .add("Passing Content", () => ( + + + + + + + + + + + + + + + Passing strings as children. + + + + + + {" "} + Hello bolded World. + + + + + )) + .add("Styling", () => ( + + + + + {" "} + Hello bolded World. + + + + + )) diff --git a/app/components/text/text.tsx b/app/components/text/text.tsx new file mode 100644 index 0000000..3ea613b --- /dev/null +++ b/app/components/text/text.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import { Text as ReactNativeText } from "react-native" +import { presets } from "./text.presets" +import { TextProps } from "./text.props" +import { translate } from "../../i18n" + +/** + * For your text displaying needs. + * + * This component is a HOC over the built-in React Native one. + */ +export function Text(props: TextProps) { + // grab the props + const { preset = "default", tx, txOptions, text, children, style: styleOverride, ...rest } = props + + // figure out which content to use + const i18nText = tx && translate(tx, txOptions) + const content = i18nText || text || children + + const style = presets[preset] || presets.default + const styles = [style, styleOverride] + + return ( + + {content} + + ) +} diff --git a/app/components/wallpaper/bg.png b/app/components/wallpaper/bg.png new file mode 100644 index 0000000..641838e Binary files /dev/null and b/app/components/wallpaper/bg.png differ diff --git a/app/components/wallpaper/bg@2x.png b/app/components/wallpaper/bg@2x.png new file mode 100644 index 0000000..3ae8396 Binary files /dev/null and b/app/components/wallpaper/bg@2x.png differ diff --git a/app/components/wallpaper/wallpaper.presets.ts b/app/components/wallpaper/wallpaper.presets.ts new file mode 100644 index 0000000..3885b8f --- /dev/null +++ b/app/components/wallpaper/wallpaper.presets.ts @@ -0,0 +1,34 @@ +import { ImageStyle } from "react-native" + +/** + * All wallpaper will start off looking like this. + */ +const BASE: ImageStyle = { + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, +} + +/** + * All the variations of wallpaper styling within the app. + * + * You want to customize these to whatever you need in your app. + */ +export const presets = { + /** + * The default wallpaper styles. + */ + stretch: { + ...BASE, + resizeMode: "stretch", + width: null, // Have to set these to null because android ¯\_(ツ)_/¯ + height: null, + } as ImageStyle, +} + +/** + * A list of preset names. + */ +export type WallpaperPresets = keyof typeof presets diff --git a/app/components/wallpaper/wallpaper.props.ts b/app/components/wallpaper/wallpaper.props.ts new file mode 100644 index 0000000..592bac9 --- /dev/null +++ b/app/components/wallpaper/wallpaper.props.ts @@ -0,0 +1,19 @@ +import { ImageStyle, StyleProp } from "react-native" +import { WallpaperPresets } from "./wallpaper.presets" + +export interface WallpaperProps { + /** + * An optional style override useful for padding & margin. + */ + style?: StyleProp + + /** + * An optional background image to override the default image. + */ + backgroundImage?: string + + /** + * One of the different types of wallpaper presets. + */ + preset?: WallpaperPresets +} diff --git a/app/components/wallpaper/wallpaper.story.tsx b/app/components/wallpaper/wallpaper.story.tsx new file mode 100644 index 0000000..8f5488a --- /dev/null +++ b/app/components/wallpaper/wallpaper.story.tsx @@ -0,0 +1,16 @@ +import * as React from "react" +import { storiesOf } from "@storybook/react-native" +import { StoryScreen, Story, UseCase } from "../../../storybook/views" +import { Wallpaper } from "./wallpaper" + +declare let module + +storiesOf("Wallpaper", module) + .addDecorator((fn) => {fn()}) + .add("Style Presets", () => ( + + + + + + )) diff --git a/app/components/wallpaper/wallpaper.tsx b/app/components/wallpaper/wallpaper.tsx new file mode 100644 index 0000000..ebba75a --- /dev/null +++ b/app/components/wallpaper/wallpaper.tsx @@ -0,0 +1,25 @@ +import React from "react" +import { AutoImage as Image } from "../auto-image/auto-image" +import { presets } from "./wallpaper.presets" +import { WallpaperProps } from "./wallpaper.props" + +const defaultImage = require("./bg.png") + +/** + * For your text displaying needs. + * + * This component is a HOC over the built-in React Native one. + */ +export function Wallpaper(props: WallpaperProps) { + // grab the props + const { preset = "stretch", style: styleOverride, backgroundImage } = props + + // assemble the style + const presetToUse = presets[preset] || presets.stretch + const styles = [presetToUse, styleOverride] + + // figure out which image to use + const source = backgroundImage || defaultImage + + return +} diff --git a/app/config/env.js b/app/config/env.js new file mode 100644 index 0000000..c5d7ffc --- /dev/null +++ b/app/config/env.js @@ -0,0 +1 @@ +module.exports = __DEV__ ? require("./env.dev") : require("./env.prod") diff --git a/app/i18n/en.json b/app/i18n/en.json new file mode 100644 index 0000000..be5c35c --- /dev/null +++ b/app/i18n/en.json @@ -0,0 +1,34 @@ +{ + "common": { + "ok": "OK!", + "cancel": "Cancel", + "back": "Back" + }, + "errors": { + "invalidEmail": "Invalid email address." + }, + "welcomeScreen": { + "poweredBy": "POWERED BY IGNITE", + "readyForLaunch": "Ready for launch.", + "continue": "CONTINUE" + }, + "demoScreen": { + "howTo": "HOW TO", + "title": "What’s In This Stack?", + "tagLine": "Congratulations, you’ve got a very advanced React Native app template here. Take advantage of this boilerplate!", + "reactotron": "Demo Reactotron", + "demoList": "Demo List", + "androidReactotronHint": "If this doesn't work, ensure the Reactotron desktop app is running, run adb reverse tcp:9090 tcp:9090 from your terminal, and reload the app.", + "iosReactotronHint": "If this doesn't work, ensure the Reactotron desktop app is running and reload app.", + "macosReactotronHint": "If this doesn't work, ensure the Reactotron desktop app is running and reload app.", + "webReactotronHint": "If this doesn't work, ensure the Reactotron desktop app is running and reload app.", + "windowsReactotronHint": "If this doesn't work, ensure the Reactotron desktop app is running and reload app." + }, + "demoListScreen": { + "title": "Demo List" + }, + "storybook": { + "placeholder": "Placeholder", + "field": "Field" + } +} diff --git a/app/i18n/i18n.ts b/app/i18n/i18n.ts new file mode 100644 index 0000000..a563bbb --- /dev/null +++ b/app/i18n/i18n.ts @@ -0,0 +1,22 @@ +import * as Localization from "expo-localization" +import i18n from "i18n-js" +import en from "./en.json" +import ja from "./ja.json" + +i18n.fallbacks = true +i18n.translations = { en, ja } + +i18n.locale = Localization.locale || "en" + +/** + * Builds up valid keypaths for translations. + * Update to your default locale of choice if not English. + */ +type DefaultLocale = typeof en +export type TxKeyPath = RecursiveKeyOf + +type RecursiveKeyOf> = { + [TKey in keyof TObj & string]: TObj[TKey] extends Record + ? `${TKey}` | `${TKey}.${RecursiveKeyOf}` + : `${TKey}` +}[keyof TObj & string] diff --git a/app/i18n/index.ts b/app/i18n/index.ts new file mode 100644 index 0000000..fbfba4e --- /dev/null +++ b/app/i18n/index.ts @@ -0,0 +1,3 @@ +import "./i18n" +export * from "./i18n" +export * from "./translate" diff --git a/app/i18n/ja.json b/app/i18n/ja.json new file mode 100644 index 0000000..7f53c8c --- /dev/null +++ b/app/i18n/ja.json @@ -0,0 +1,7 @@ +{ + "common": { + "ok": "OK 🇯🇵", + "cancel": "Cancel 🇯🇵", + "back": "Back 🇯🇵" + } +} diff --git a/app/i18n/translate.ts b/app/i18n/translate.ts new file mode 100644 index 0000000..ef1a019 --- /dev/null +++ b/app/i18n/translate.ts @@ -0,0 +1,11 @@ +import i18n from "i18n-js" +import { TxKeyPath } from "./i18n" + +/** + * Translates text. + * + * @param key The i18n key. + */ +export function translate(key: TxKeyPath, options?: i18n.TranslateOptions) { + return key ? i18n.t(key, options) : null +} diff --git a/app/models/character-store/character-store.test.ts b/app/models/character-store/character-store.test.ts new file mode 100644 index 0000000..6d82079 --- /dev/null +++ b/app/models/character-store/character-store.test.ts @@ -0,0 +1,7 @@ +import { CharacterStoreModel } from "./character-store" + +test("can be created", () => { + const instance = CharacterStoreModel.create({}) + + expect(instance).toBeTruthy() +}) diff --git a/app/models/character-store/character-store.ts b/app/models/character-store/character-store.ts new file mode 100644 index 0000000..bd92615 --- /dev/null +++ b/app/models/character-store/character-store.ts @@ -0,0 +1,37 @@ +import { Instance, SnapshotOut, types } from "mobx-state-tree" +import { CharacterModel, CharacterSnapshot } from "../character/character" +import { CharacterApi } from "../../services/api/character-api" +import { withEnvironment } from "../extensions/with-environment" + +/** + * Example store containing Rick and Morty characters + */ +export const CharacterStoreModel = types + .model("CharacterStore") + .props({ + characters: types.optional(types.array(CharacterModel), []), + }) + .extend(withEnvironment) + .actions((self) => ({ + saveCharacters: (characterSnapshots: CharacterSnapshot[]) => { + self.characters.replace(characterSnapshots) + }, + })) + .actions((self) => ({ + getCharacters: async () => { + const characterApi = new CharacterApi(self.environment.api) + const result = await characterApi.getCharacters() + + if (result.kind === "ok") { + self.saveCharacters(result.characters) + } else { + __DEV__ && console.tron.log(result.kind) + } + }, + })) + +type CharacterStoreType = Instance +export interface CharacterStore extends CharacterStoreType {} +type CharacterStoreSnapshotType = SnapshotOut +export interface CharacterStoreSnapshot extends CharacterStoreSnapshotType {} +export const createCharacterStoreDefaultModel = () => types.optional(CharacterStoreModel, {}) diff --git a/app/models/character/character.test.ts b/app/models/character/character.test.ts new file mode 100644 index 0000000..2f330b4 --- /dev/null +++ b/app/models/character/character.test.ts @@ -0,0 +1,10 @@ +import { CharacterModel } from "./character" + +test("can be created", () => { + const instance = CharacterModel.create({ + id: 1, + name: "Rick Sanchez", + }) + + expect(instance).toBeTruthy() +}) diff --git a/app/models/character/character.ts b/app/models/character/character.ts new file mode 100644 index 0000000..ca3cbc7 --- /dev/null +++ b/app/models/character/character.ts @@ -0,0 +1,17 @@ +import { Instance, SnapshotOut, types } from "mobx-state-tree" + +/** + * Rick and Morty character model. + */ +export const CharacterModel = types.model("Character").props({ + id: types.identifierNumber, + name: types.maybe(types.string), + status: types.maybe(types.string), + image: types.maybe(types.string), +}) + +type CharacterType = Instance +export interface Character extends CharacterType {} +type CharacterSnapshotType = SnapshotOut +export interface CharacterSnapshot extends CharacterSnapshotType {} +export const createCharacterDefaultModel = () => types.optional(CharacterModel, {}) diff --git a/app/models/environment.ts b/app/models/environment.ts new file mode 100644 index 0000000..e8569b1 --- /dev/null +++ b/app/models/environment.ts @@ -0,0 +1,40 @@ +import { Api } from "../services/api" + +let ReactotronDev +if (__DEV__) { + const { Reactotron } = require("../services/reactotron") + ReactotronDev = Reactotron +} + +/** + * The environment is a place where services and shared dependencies between + * models live. They are made available to every model via dependency injection. + */ +export class Environment { + constructor() { + // create each service + if (__DEV__) { + // dev-only services + this.reactotron = new ReactotronDev() + } + this.api = new Api() + } + + async setup() { + // allow each service to setup + if (__DEV__) { + await this.reactotron.setup() + } + await this.api.setup() + } + + /** + * Reactotron is only available in dev. + */ + reactotron: typeof ReactotronDev + + /** + * Our api. + */ + api: Api +} diff --git a/app/models/extensions/with-environment.ts b/app/models/extensions/with-environment.ts new file mode 100644 index 0000000..9fe5fd5 --- /dev/null +++ b/app/models/extensions/with-environment.ts @@ -0,0 +1,17 @@ +import { getEnv, IStateTreeNode } from "mobx-state-tree" +import { Environment } from "../environment" + +/** + * Adds a environment property to the node for accessing our + * Environment in strongly typed. + */ +export const withEnvironment = (self: IStateTreeNode) => ({ + views: { + /** + * The environment. + */ + get environment() { + return getEnv(self) + }, + }, +}) diff --git a/app/models/extensions/with-root-store.ts b/app/models/extensions/with-root-store.ts new file mode 100644 index 0000000..eff769c --- /dev/null +++ b/app/models/extensions/with-root-store.ts @@ -0,0 +1,17 @@ +import { getRoot, IStateTreeNode } from "mobx-state-tree" +import { RootStoreModel } from "../root-store/root-store" + +/** + * Adds a rootStore property to the node for a convenient + * and strongly typed way for stores to access other stores. + */ +export const withRootStore = (self: IStateTreeNode) => ({ + views: { + /** + * The root store. + */ + get rootStore() { + return getRoot(self) + }, + }, +}) diff --git a/app/models/index.ts b/app/models/index.ts new file mode 100644 index 0000000..3538dbb --- /dev/null +++ b/app/models/index.ts @@ -0,0 +1,5 @@ +export * from "./extensions/with-environment" +export * from "./extensions/with-root-store" +export * from "./root-store/root-store" +export * from "./root-store/root-store-context" +export * from "./root-store/setup-root-store" diff --git a/app/models/root-store/root-store-context.ts b/app/models/root-store/root-store-context.ts new file mode 100644 index 0000000..537e51c --- /dev/null +++ b/app/models/root-store/root-store-context.ts @@ -0,0 +1,22 @@ +import { createContext, useContext } from "react" +import { RootStore } from "./root-store" + +/** + * Create a context we can use to + * - Provide access to our stores from our root component + * - Consume stores in our screens (or other components, though it's + * preferable to just connect screens) + */ +const RootStoreContext = createContext({} as RootStore) + +/** + * The provider our root component will use to expose the root store + */ +export const RootStoreProvider = RootStoreContext.Provider + +/** + * A hook that screens can use to gain access to our stores, with + * `const { someStore, someOtherStore } = useStores()`, + * or less likely: `const rootStore = useStores()` + */ +export const useStores = () => useContext(RootStoreContext) diff --git a/app/models/root-store/root-store.ts b/app/models/root-store/root-store.ts new file mode 100644 index 0000000..1131b48 --- /dev/null +++ b/app/models/root-store/root-store.ts @@ -0,0 +1,20 @@ +import { Instance, SnapshotOut, types } from "mobx-state-tree" +import { CharacterStoreModel } from "../character-store/character-store" + +/** + * A RootStore model. + */ +// prettier-ignore +export const RootStoreModel = types.model("RootStore").props({ + characterStore: types.optional(CharacterStoreModel, {} as any), +}) + +/** + * The RootStore instance. + */ +export interface RootStore extends Instance {} + +/** + * The data of a RootStore. + */ +export interface RootStoreSnapshot extends SnapshotOut {} diff --git a/app/models/root-store/setup-root-store.ts b/app/models/root-store/setup-root-store.ts new file mode 100644 index 0000000..4a6d0c5 --- /dev/null +++ b/app/models/root-store/setup-root-store.ts @@ -0,0 +1,55 @@ +import { onSnapshot } from "mobx-state-tree" +import { RootStoreModel, RootStore } from "./root-store" +import { Environment } from "../environment" +import * as storage from "../../utils/storage" + +/** + * The key we'll be saving our state as within async storage. + */ +const ROOT_STATE_STORAGE_KEY = "root" + +/** + * Setup the environment that all the models will be sharing. + * + * The environment includes other functions that will be picked from some + * of the models that get created later. This is how we loosly couple things + * like events between models. + */ +export async function createEnvironment() { + const env = new Environment() + await env.setup() + return env +} + +/** + * Setup the root state. + */ +export async function setupRootStore() { + let rootStore: RootStore + let data: any + + // prepare the environment that will be associated with the RootStore. + const env = await createEnvironment() + try { + // load data from storage + data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {} + rootStore = RootStoreModel.create(data, env) + } catch (e) { + // if there's any problems loading, then let's at least fallback to an empty state + // instead of crashing. + rootStore = RootStoreModel.create({}, env) + + // but please inform us what happened + __DEV__ && console.tron.error(e.message, null) + } + + // reactotron logging + if (__DEV__) { + env.reactotron.setRootStore(rootStore, data) + } + + // track changes & save to storage + onSnapshot(rootStore, (snapshot) => storage.save(ROOT_STATE_STORAGE_KEY, snapshot)) + + return rootStore +} diff --git a/app/navigators/index.ts b/app/navigators/index.ts new file mode 100644 index 0000000..b1b89a2 --- /dev/null +++ b/app/navigators/index.ts @@ -0,0 +1,4 @@ +export * from "./main-navigator" +export * from "./root-navigator" +export * from "./navigation-utilities" +// export other navigators from here diff --git a/app/navigators/main-navigator.tsx b/app/navigators/main-navigator.tsx new file mode 100644 index 0000000..9ad110a --- /dev/null +++ b/app/navigators/main-navigator.tsx @@ -0,0 +1,57 @@ +/** + * This is the navigator you will modify to display the logged-in screens of your app. + * You can use RootNavigator to also display an auth flow or other user flows. + * + * You'll likely spend most of your time in this file. + */ +import React from "react" +import { createStackNavigator } from "@react-navigation/stack" +import { WelcomeScreen, DemoScreen, DemoListScreen } from "../screens" + +/** + * This type allows TypeScript to know what routes are defined in this navigator + * as well as what properties (if any) they might take when navigating to them. + * + * If no params are allowed, pass through `undefined`. Generally speaking, we + * recommend using your MobX-State-Tree store(s) to keep application state + * rather than passing state through navigation params. + * + * For more information, see this documentation: + * https://reactnavigation.org/docs/params/ + * https://reactnavigation.org/docs/typescript#type-checking-the-navigator + */ +export type PrimaryParamList = { + welcome: undefined + demo: undefined + demoList: undefined +} + +// Documentation: https://reactnavigation.org/docs/stack-navigator/ +const Stack = createStackNavigator() + +export function MainNavigator() { + return ( + + + + + + ) +} + +/** + * A list of routes from which we're allowed to leave the app when + * the user presses the back button on Android. + * + * Anything not on this list will be a standard `back` action in + * react-navigation. + * + * `canExit` is used in ./app/app.tsx in the `useBackButtonHandler` hook. + */ +const exitRoutes = ["welcome"] +export const canExit = (routeName: string) => exitRoutes.includes(routeName) diff --git a/app/navigators/navigation-utilities.tsx b/app/navigators/navigation-utilities.tsx new file mode 100644 index 0000000..de1ea05 --- /dev/null +++ b/app/navigators/navigation-utilities.tsx @@ -0,0 +1,127 @@ +import React, { useState, useEffect, useRef } from "react" +import { BackHandler } from "react-native" +import { PartialState, NavigationState, NavigationContainerRef } from "@react-navigation/native" + +export const RootNavigation = { + navigate(name: string) { + name // eslint-disable-line no-unused-expressions + }, + goBack() {}, // eslint-disable-line @typescript-eslint/no-empty-function + resetRoot(state?: PartialState | NavigationState) {}, // eslint-disable-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + getRootState(): NavigationState { + return {} as any + }, +} + +export const setRootNavigation = (ref: React.RefObject) => { + for (const method in RootNavigation) { + RootNavigation[method] = (...args: any) => { + if (ref.current) { + return ref.current[method](...args) + } + } + } +} + +/** + * Gets the current screen from any navigation state. + */ +export function getActiveRouteName(state: NavigationState | PartialState) { + const route = state.routes[state.index] + + // Found the active route -- return the name + if (!route.state) return route.name + + // Recursive call to deal with nested routers + return getActiveRouteName(route.state) +} + +/** + * Hook that handles Android back button presses and forwards those on to + * the navigation or allows exiting the app. + */ +export function useBackButtonHandler( + ref: React.RefObject, + canExit: (routeName: string) => boolean, +) { + const canExitRef = useRef(canExit) + + useEffect(() => { + canExitRef.current = canExit + }, [canExit]) + + useEffect(() => { + // We'll fire this when the back button is pressed on Android. + const onBackPress = () => { + const navigation = ref.current + + if (navigation == null) { + return false + } + + // grab the current route + const routeName = getActiveRouteName(navigation.getRootState()) + + // are we allowed to exit? + if (canExitRef.current(routeName)) { + // let the system know we've not handled this event + return false + } + + // we can't exit, so let's turn this into a back action + if (navigation.canGoBack()) { + navigation.goBack() + + return true + } + + return false + } + + // Subscribe when we come to life + BackHandler.addEventListener("hardwareBackPress", onBackPress) + + // Unsubscribe when we're done + return () => BackHandler.removeEventListener("hardwareBackPress", onBackPress) + }, [ref]) +} + +/** + * Custom hook for persisting navigation state. + */ +export function useNavigationPersistence(storage: any, persistenceKey: string) { + const [initialNavigationState, setInitialNavigationState] = useState() + const [isRestoringNavigationState, setIsRestoringNavigationState] = useState(true) + + const routeNameRef = useRef() + const onNavigationStateChange = (state) => { + const previousRouteName = routeNameRef.current + const currentRouteName = getActiveRouteName(state) + + if (previousRouteName !== currentRouteName) { + // track screens. + __DEV__ && console.tron.log(currentRouteName) + } + + // Save the current route name for later comparision + routeNameRef.current = currentRouteName + + // Persist state to storage + storage.save(persistenceKey, state) + } + + const restoreState = async () => { + try { + const state = await storage.load(persistenceKey) + if (state) setInitialNavigationState(state) + } finally { + setIsRestoringNavigationState(false) + } + } + + useEffect(() => { + if (isRestoringNavigationState) restoreState() + }, [isRestoringNavigationState]) + + return { onNavigationStateChange, restoreState, initialNavigationState } +} diff --git a/app/navigators/root-navigator.tsx b/app/navigators/root-navigator.tsx new file mode 100644 index 0000000..2c04f29 --- /dev/null +++ b/app/navigators/root-navigator.tsx @@ -0,0 +1,59 @@ +/** + * The root navigator is used to switch between major navigation flows of your app. + * Generally speaking, it will contain an auth flow (registration, login, forgot password) + * and a "main" flow (which is contained in your MainNavigator) which the user + * will use once logged in. + */ +import React from "react" +import { NavigationContainer, NavigationContainerRef } from "@react-navigation/native" +import { createStackNavigator } from "@react-navigation/stack" +import { MainNavigator } from "./main-navigator" +import { color } from "../theme" + +/** + * This type allows TypeScript to know what routes are defined in this navigator + * as well as what properties (if any) they might take when navigating to them. + * + * We recommend using MobX-State-Tree store(s) to handle state rather than navigation params. + * + * For more information, see this documentation: + * https://reactnavigation.org/docs/params/ + * https://reactnavigation.org/docs/typescript#type-checking-the-navigator + */ +export type RootParamList = { + mainStack: undefined +} + +const Stack = createStackNavigator() + +const RootStack = () => { + return ( + + + + ) +} + +export const RootNavigator = React.forwardRef< + NavigationContainerRef, + Partial> +>((props, ref) => { + return ( + + + + ) +}) + +RootNavigator.displayName = "RootNavigator" diff --git a/app/screens/demo/demo-list-screen.tsx b/app/screens/demo/demo-list-screen.tsx new file mode 100644 index 0000000..ae66307 --- /dev/null +++ b/app/screens/demo/demo-list-screen.tsx @@ -0,0 +1,86 @@ +import React, { useEffect } from "react" +import { FlatList, TextStyle, View, ViewStyle, ImageStyle } from "react-native" +import { useNavigation } from "@react-navigation/native" +import { observer } from "mobx-react-lite" +import { Header, Screen, Text, Wallpaper, AutoImage as Image } from "../../components" +import { color, spacing } from "../../theme" +import { useStores } from "../../models" + +const FULL: ViewStyle = { + flex: 1, +} +const CONTAINER: ViewStyle = { + backgroundColor: color.transparent, +} +const HEADER: TextStyle = { + paddingBottom: spacing[5] - 1, + paddingHorizontal: spacing[4], + paddingTop: spacing[3], +} +const HEADER_TITLE: TextStyle = { + fontSize: 12, + fontWeight: "bold", + letterSpacing: 1.5, + lineHeight: 15, + textAlign: "center", +} +const LIST_CONTAINER: ViewStyle = { + alignItems: "center", + flexDirection: "row", + padding: 10, +} +const IMAGE: ImageStyle = { + borderRadius: 35, + height: 65, + width: 65, +} +const LIST_TEXT: TextStyle = { + marginLeft: 10, +} +const FLAT_LIST: ViewStyle = { + paddingHorizontal: spacing[4], +} + +export const DemoListScreen = observer(function DemoListScreen() { + const navigation = useNavigation() + const goBack = () => navigation.goBack() + + const { characterStore } = useStores() + const { characters } = characterStore + + useEffect(() => { + async function fetchData() { + await characterStore.getCharacters() + } + + fetchData() + }, []) + + return ( + + + +
+ String(item.id)} + renderItem={({ item }) => ( + + + + {item.name} ({item.status}) + + + )} + /> + + + ) +}) diff --git a/app/screens/demo/demo-screen.tsx b/app/screens/demo/demo-screen.tsx new file mode 100644 index 0000000..40e3830 --- /dev/null +++ b/app/screens/demo/demo-screen.tsx @@ -0,0 +1,181 @@ +import React from "react" +import { ImageStyle, Platform, TextStyle, View, ViewStyle } from "react-native" +import { useNavigation } from "@react-navigation/native" +import { observer } from "mobx-react-lite" +import { + BulletItem, + Button, + Header, + Text, + Screen, + Wallpaper, + AutoImage as Image, +} from "../../components" +import { color, spacing } from "../../theme" +import { Api } from "../../services/api" +import { save } from "../../utils/storage" +export const logoIgnite = require("./logo-ignite.png") +export const heart = require("./heart.png") + +const FULL: ViewStyle = { flex: 1 } +const CONTAINER: ViewStyle = { + backgroundColor: color.transparent, + paddingHorizontal: spacing[4], +} +const DEMO: ViewStyle = { + paddingVertical: spacing[4], + paddingHorizontal: spacing[4], + backgroundColor: color.palette.deepPurple, +} +const BOLD: TextStyle = { fontWeight: "bold" } +const DEMO_TEXT: TextStyle = { + ...BOLD, + fontSize: 13, + letterSpacing: 2, +} +const HEADER: TextStyle = { + paddingTop: spacing[3], + paddingBottom: spacing[5] - 1, + paddingHorizontal: 0, +} +const HEADER_TITLE: TextStyle = { + ...BOLD, + fontSize: 12, + lineHeight: 15, + textAlign: "center", + letterSpacing: 1.5, +} +const TITLE: TextStyle = { + ...BOLD, + fontSize: 28, + lineHeight: 38, + textAlign: "center", + marginBottom: spacing[5], +} +const TAGLINE: TextStyle = { + color: "#BAB6C8", + fontSize: 15, + lineHeight: 22, + marginBottom: spacing[4] + spacing[1], +} +const IGNITE: ImageStyle = { + marginVertical: spacing[6], + alignSelf: "center", + width: 180, + height: 100, +} +const LOVE_WRAPPER: ViewStyle = { + flexDirection: "row", + alignItems: "center", + alignSelf: "center", +} +const LOVE: TextStyle = { + color: "#BAB6C8", + fontSize: 15, + lineHeight: 22, +} +const HEART: ImageStyle = { + marginHorizontal: spacing[2], + width: 10, + height: 10, + resizeMode: "contain", +} +const HINT: TextStyle = { + color: "#BAB6C8", + fontSize: 12, + lineHeight: 15, + marginVertical: spacing[2], +} + +const platformCommand = Platform.select({ + ios: "Cmd + D", + android: "Cmd/Ctrl + M", +}) + +export const DemoScreen = observer(function DemoScreen() { + const navigation = useNavigation() + const goBack = () => navigation.goBack() + + const demoReactotron = React.useMemo( + () => async () => { + console.tron.log("Your Friendly tron log message") + console.tron.logImportant("I am important") + console.tron.display({ + name: "DISPLAY", + value: { + numbers: 1, + strings: "strings", + booleans: true, + arrays: [1, 2, 3], + objects: { + deeper: { + deeper: { + yay: "👾", + }, + }, + }, + functionNames: function hello() { + /* dummy function */ + }, + }, + preview: "More control with display()", + important: true, + image: { + uri: + "https://avatars2.githubusercontent.com/u/3902527?s=200&u=a0d16b13ed719f35d95ca0f4440f5d07c32c349a&v=4", + }, + }) + // make an API call for the demo + // Don't do API like this, use store's API + const demo = new Api() + demo.setup() + demo.getUser("1") + // Let's do some async storage stuff + await save("Cool Name", "Boaty McBoatface") + }, + [], + ) + + return ( + + + +
+ + + + + + +