├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── deploy_server_do.sh ├── deploy_web.sh ├── lerna.json ├── mock.sql ├── ormconfig.json ├── package.json ├── packages ├── app │ ├── .babelrc │ ├── .watchmanconfig │ ├── App.tsx │ ├── app.json │ ├── assets │ │ ├── icon.png │ │ └── splash.png │ ├── package.json │ ├── rn-cli.config.js │ ├── src │ │ ├── apollo.ts │ │ ├── index.tsx │ │ ├── modules │ │ │ ├── listing │ │ │ │ ├── create │ │ │ │ │ └── CreateListingConnector.tsx │ │ │ │ └── find │ │ │ │ │ └── FindListingsConnector.tsx │ │ │ ├── login │ │ │ │ ├── LoginConnector.tsx │ │ │ │ └── ui │ │ │ │ │ └── LoginView.tsx │ │ │ ├── me │ │ │ │ └── Me.tsx │ │ │ ├── register │ │ │ │ ├── RegisterConnector.tsx │ │ │ │ └── ui │ │ │ │ │ └── RegisterView.tsx │ │ │ └── shared │ │ │ │ ├── CheckboxGroupField.tsx │ │ │ │ ├── InputField.tsx │ │ │ │ ├── PictureField.tsx │ │ │ │ └── constants.ts │ │ └── routes │ │ │ └── index.tsx │ └── tsconfig.json ├── common │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── yupSchemas │ │ │ └── user.ts │ ├── tsconfig.json │ └── tslint.json ├── controller │ ├── gen-types.sh │ ├── package.json │ ├── schema.json │ ├── src │ │ ├── index.ts │ │ ├── modules │ │ │ ├── ChangePasswordController │ │ │ │ └── index.tsx │ │ │ ├── CreateListing │ │ │ │ └── index.tsx │ │ │ ├── CreateMessage │ │ │ │ └── index.tsx │ │ │ ├── FindListings │ │ │ │ └── index.tsx │ │ │ ├── ForgotPasswordController │ │ │ │ └── index.tsx │ │ │ ├── LoginController │ │ │ │ └── index.tsx │ │ │ ├── LogoutController │ │ │ │ └── index.tsx │ │ │ ├── RegisterController │ │ │ │ └── index.tsx │ │ │ ├── SearchListings │ │ │ │ └── index.tsx │ │ │ ├── UpdateListing │ │ │ │ └── index.tsx │ │ │ ├── ViewListing │ │ │ │ └── index.tsx │ │ │ ├── ViewMessages │ │ │ │ └── index.tsx │ │ │ └── auth │ │ │ │ └── AuthRoute.tsx │ │ ├── schemaTypes.ts │ │ ├── types │ │ │ └── NormalizedErrorMap.ts │ │ └── utils │ │ │ └── normalizeErrors.ts │ ├── tsconfig.json │ └── tslint.json ├── server │ ├── .env.example │ ├── README.md │ ├── ormconfig.json │ ├── package.json │ ├── src │ │ ├── constants.ts │ │ ├── entity │ │ │ ├── Listing.ts │ │ │ ├── Message.ts │ │ │ └── User.ts │ │ ├── index.ts │ │ ├── loaders │ │ │ └── UserLoader.ts │ │ ├── middleware.ts │ │ ├── migration │ │ │ ├── 1531437372575-ListingTable.ts │ │ │ ├── 1531437838812-ListingUser.ts │ │ │ └── 1531438091342-ListingUser.ts │ │ ├── modules │ │ │ ├── date │ │ │ │ ├── resolvers.ts │ │ │ │ └── schema.graphql │ │ │ ├── listing │ │ │ │ ├── create │ │ │ │ │ ├── resolvers.ts │ │ │ │ │ └── schema.graphql │ │ │ │ ├── delete │ │ │ │ │ ├── resolvers.ts │ │ │ │ │ └── schema.graphql │ │ │ │ ├── find │ │ │ │ │ ├── resolvers.ts │ │ │ │ │ └── schema.graphql │ │ │ │ ├── search │ │ │ │ │ ├── resolvers.ts │ │ │ │ │ └── schema.graphql │ │ │ │ ├── shared │ │ │ │ │ └── processUpload.ts │ │ │ │ ├── update │ │ │ │ │ ├── resolvers.ts │ │ │ │ │ └── schema.graphql │ │ │ │ └── view │ │ │ │ │ ├── resolvers.ts │ │ │ │ │ └── schema.graphql │ │ │ ├── message │ │ │ │ ├── create │ │ │ │ │ ├── resolvers.ts │ │ │ │ │ └── schema.graphql │ │ │ │ ├── find │ │ │ │ │ ├── resolvers.ts │ │ │ │ │ └── schema.graphql │ │ │ │ ├── newMessage │ │ │ │ │ ├── resolvers.ts │ │ │ │ │ └── schema.graphql │ │ │ │ └── shared │ │ │ │ │ └── constants.ts │ │ │ ├── shared │ │ │ │ └── isAuthenticated.ts │ │ │ └── user │ │ │ │ ├── forgotPassword │ │ │ │ ├── errorMessages.ts │ │ │ │ ├── forgotPassword.test.ts │ │ │ │ ├── resolvers.ts │ │ │ │ └── schema.graphql │ │ │ │ ├── login │ │ │ │ ├── errorMessages.ts │ │ │ │ ├── login.test.ts │ │ │ │ ├── resolvers.ts │ │ │ │ └── schema.graphql │ │ │ │ ├── logout │ │ │ │ ├── logout.test.ts │ │ │ │ ├── resolvers.ts │ │ │ │ └── schema.graphql │ │ │ │ ├── me │ │ │ │ ├── me.test.ts │ │ │ │ ├── middleware.ts │ │ │ │ ├── resolvers.ts │ │ │ │ └── schema.graphql │ │ │ │ ├── register │ │ │ │ ├── createConfirmEmailLink.test.ts │ │ │ │ ├── createConfirmEmailLink.ts │ │ │ │ ├── errorMessages.ts │ │ │ │ ├── register.test.ts │ │ │ │ ├── resolvers.ts │ │ │ │ └── schema.graphql │ │ │ │ └── shared │ │ │ │ ├── Error.graphql │ │ │ │ └── User.graphql │ │ ├── redis.ts │ │ ├── routes │ │ │ ├── confirmEmail.test.ts │ │ │ └── confirmEmail.ts │ │ ├── scripts │ │ │ └── createTypes.ts │ │ ├── shield.ts │ │ ├── startServer.ts │ │ ├── testUtils │ │ │ ├── callSetup.js │ │ │ ├── createTestConn.ts │ │ │ └── setup.ts │ │ ├── types │ │ │ ├── graphql-utils.ts │ │ │ ├── merge-graphql-schemas.d.ts │ │ │ ├── rate-limit-redis.d.ts │ │ │ └── schema.d.ts │ │ └── utils │ │ │ ├── TestClient.ts │ │ │ ├── createForgotPasswordLink.ts │ │ │ ├── createMiddleware.ts │ │ │ ├── createTypeormConn.ts │ │ │ ├── forgotPasswordLockAccount.ts │ │ │ ├── formatYupError.ts │ │ │ ├── genSchema.ts │ │ │ ├── removeAllUsersSessions.ts │ │ │ └── sendEmail.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock └── web │ ├── .env.development │ ├── .env.production │ ├── images.d.ts │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── _redirects │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ ├── src │ ├── apollo.ts │ ├── index.css │ ├── index.tsx │ ├── modules │ │ ├── TextPage │ │ │ └── index.tsx │ │ ├── changePassword │ │ │ ├── ChangePasswordConnector.tsx │ │ │ └── ui │ │ │ │ └── ChangePasswordView.tsx │ │ ├── forgotPassword │ │ │ ├── ForgotPasswordConnector.tsx │ │ │ └── ui │ │ │ │ └── ForgotPasswordView.tsx │ │ ├── listing │ │ │ ├── create │ │ │ │ └── CreateListingConnector.tsx │ │ │ ├── delete │ │ │ │ └── DemoDelete.tsx │ │ │ ├── edit │ │ │ │ └── EditListingConnector.tsx │ │ │ ├── find │ │ │ │ └── FindListingsConnector.tsx │ │ │ ├── messages │ │ │ │ ├── InputBar.tsx │ │ │ │ └── MessageConnector.tsx │ │ │ ├── shared │ │ │ │ ├── ListingForm.tsx │ │ │ │ └── ui │ │ │ │ │ ├── Page1.tsx │ │ │ │ │ ├── Page2.tsx │ │ │ │ │ └── Page3.tsx │ │ │ └── view │ │ │ │ └── ViewListingConnector.tsx │ │ ├── login │ │ │ ├── LoginConnector.tsx │ │ │ └── ui │ │ │ │ └── LoginView.tsx │ │ ├── logout │ │ │ ├── CallLogout.tsx │ │ │ └── index.tsx │ │ ├── register │ │ │ ├── RegisterConnector.tsx │ │ │ └── ui │ │ │ │ └── RegisterView.tsx │ │ └── shared │ │ │ ├── DropzoneField.tsx │ │ │ ├── InputField.tsx │ │ │ ├── LocationField.tsx │ │ │ ├── TagField.tsx │ │ │ └── geo.css │ ├── registerServiceWorker.ts │ ├── routes │ │ └── index.tsx │ └── tsconfig.json │ ├── tsconfig.json │ ├── tsconfig.prod.json │ ├── tsconfig.test.json │ └── tslint.json ├── postinstall.sh ├── tslint.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !./package.json 3 | !./packages/server/package.json 4 | !./packages/common/package.json 5 | !./packages/server/dist 6 | !./packages/common/dist 7 | !./packages/server/.env.prod 8 | !./packages/server/.env.example 9 | !./ormconfig.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .vscode/ 4 | .expo/ 5 | .netlify 6 | 7 | node_modules/ 8 | build/ 9 | temp/ 10 | dist/ 11 | 12 | .env 13 | .env.* 14 | !.env.example 15 | 16 | dump.rdb 17 | *.log 18 | npm-debug.* 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | images/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | WORKDIR /abb 4 | 5 | COPY ./package.json . 6 | COPY ./packages/server/package.json ./packages/server/ 7 | COPY ./packages/common/package.json ./packages/common/ 8 | 9 | RUN npm i -g yarn 10 | RUN yarn install --production 11 | 12 | COPY ./packages/server/dist ./packages/server/dist 13 | COPY ./packages/common/dist ./packages/common/dist 14 | COPY ./packages/server/.env.prod ./packages/server/.env 15 | COPY ./packages/server/.env.example ./packages/server/ 16 | COPY ./ormconfig.json . 17 | 18 | WORKDIR ./packages/server 19 | 20 | ENV NODE_ENV production 21 | 22 | EXPOSE 4000 23 | 24 | CMD ["node", "dist/index.js"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ben Awad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: cd packages/server && node dist/server/src/index.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fullstack-graphql-airbnb-clone 2 | 3 | A Fullstack GraphQL Airbnb Clone with React and React Native. 4 | 5 | - Branches are in the order they were coded. 6 | - Watch how this was made: https://www.youtube.com/playlist?list=PLN3n1USn4xlnfJIQBa6bBjjiECnk6zL6s 7 | - This builds off the GraphQL Typescript Server I made: https://github.com/benawad/graphql-ts-server-boilerplate. 8 | - You can see the YouTube Playlist for how that was made here: https://www.youtube.com/playlist?list=PLN3n1USn4xlky9uj6wOhfsPez7KZOqm2V 9 | - Join the Discord: https://discord.gg/Vehs99V 10 | 11 | ## Packages 12 | 13 | This project is made up of 5 packages that share code using Yarn Workspaces. 14 | 15 | - web (React.js website) 16 | - app (React Native app) 17 | - server (GraphQL Typescript server) 18 | - common (Code shared between web, app, and server) 19 | - controller (Components shared between web and app) 20 | 21 | ## Installation 22 | 23 | 1. Clone project 24 | 25 | ``` 26 | git clone https://github.com/benawad/fullstack-graphql-airbnb-clone.git 27 | ``` 28 | 29 | 2. cd into folder 30 | 31 | ``` 32 | cd fullstack-graphql-airbnb-clone 33 | ``` 34 | 35 | 3. Download dependencies 36 | 37 | ``` 38 | yarn 39 | ``` 40 | 41 | 4. Start PostgreSQL server 42 | 5. Create database called `graphql-ts-server-boilerplate` 43 | 44 | ``` 45 | createdb graphql-ts-server-boilerplate 46 | ``` 47 | 48 | 6. [Add a user](https://medium.com/coding-blocks/creating-user-database-and-adding-access-on-postgresql-8bfcd2f4a91e) with the username `postgres` and and no password. (You can change what these values are in the [ormconfig.json](https://github.com/benawad/graphql-ts-server-boilerplate/blob/master/ormconfig.json)) 49 | 50 | 7. Connect to the database with `psql` and add the uuid extension: 51 | 52 | ``` 53 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 54 | ``` 55 | 56 | 8. Install and start Redis 57 | 58 | 9. In `packages/server` create a file called `.env` and add the following line inside: `FRONTEND_HOST=http://localhost:3000` 59 | 60 | 10. Run `yarn build` in `packages/common` 61 | 62 | 11. Run `yarn build` in `packages/controller` 63 | 64 | 12. Get Google Maps API key and put it here https://github.com/benawad/fullstack-graphql-airbnb-clone/blob/master/packages/web/public/index.html#L14 Videos doing that: https://youtu.be/-QQnzDVcTCo and https://youtu.be/xLlIgokKiLc 65 | 66 | 67 | 68 | ## Usage 69 | 70 | 1. Start server `yarn start` in `packages/server` 71 | 72 | 2. Now you can run `yarn start` in `packages/web` or `packages/app` to start the website or app. 73 | 74 | 3. How to get credentials working in graphql playground: https://youtu.be/oM-EmNdhwI4?t=8m39s 75 | 76 | ## Deploy 77 | 78 | ### Server 79 | 80 | 1. https://www.youtube.com/watch?v=qQAozc1MkdU 81 | 2. https://www.youtube.com/watch?v=0t-rE5wUP-E 82 | 83 | ### Website 84 | 85 | 1. https://www.youtube.com/watch?v=FiU3SHEaFwk 86 | 2. https://www.youtube.com/watch?v=vPu1sfuYFzw 87 | 3. https://www.youtube.com/watch?v=Ry6Zobb-kaw 88 | 89 | ## Features 90 | 91 | 1. Website register/login 92 | 2. Deploy backend and frontend 93 | 3. App register/login 94 | 4. Website and App forgot password 95 | 5. Website and App create listing 96 | 6. Website and App view listings 97 | 7. logout 98 | 8. Website chat 99 | -------------------------------------------------------------------------------- /deploy_server_do.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | yarn build:server 3 | heroku container:push --app=calm-citadel-25445 web 4 | heroku container:release --app=calm-citadel-25445 web 5 | # docker build -t benawad/abb:latest . 6 | # docker push benawad/abb:latest 7 | # ssh root@167.99.11.233 "docker pull benawad/abb:latest && docker tag benawad/abb:latest dokku/abb:latest && dokku tags:deploy abb latest" -------------------------------------------------------------------------------- /deploy_web.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | yarn build:web 3 | netlify deploy -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.11.0", 3 | "packages": ["packages/*"], 4 | "npmClient": "yarn", 5 | "useWorkspaces": true, 6 | "version": "0.0.0" 7 | } 8 | -------------------------------------------------------------------------------- /mock.sql: -------------------------------------------------------------------------------- 1 | insert into listings (id, name, category, "pictureUrl", description, price, beds, guests, latitude, longitude, amenities, "userId") values ('ed110dda-0f42-4e20-b43e-8d7f5f869048', 'Buzzbean', 'condo', 'http://dummyimage.com/138x113.png/ff4444/ffffff', 'lacinia nisi venenatis tristique', 423, 3, 4, 24.0527442, -74.5302159, '{}', '21c31c50-9627-4cca-ae66-35be19ef2c7b'); -------------------------------------------------------------------------------- /ormconfig.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "production", 4 | "type": "postgres", 5 | "synchronize": true, 6 | "logging": true 7 | }, 8 | { 9 | "name": "development", 10 | "type": "postgres", 11 | "host": "localhost", 12 | "port": 5432, 13 | "username": "postgres", 14 | "password": "postgres", 15 | "database": "graphql-ts-server-boilerplate", 16 | "synchronize": true, 17 | "logging": true, 18 | "entities": ["src/entity/**/*.ts"], 19 | "migrations": ["src/migration/**/*.ts"], 20 | "subscribers": ["src/subscriber/**/*.ts"], 21 | "cli": { 22 | "entitiesDir": "src/entity", 23 | "migrationsDir": "src/migration", 24 | "subscribersDir": "src/subscriber" 25 | } 26 | }, 27 | { 28 | "name": "test", 29 | "type": "postgres", 30 | "host": "localhost", 31 | "port": 5432, 32 | "username": "postgres", 33 | "password": "postgres", 34 | "database": "graphql-ts-server-boilerplate-test", 35 | "synchronize": true, 36 | "logging": false, 37 | "dropSchema": true, 38 | "entities": ["src/entity/**/*.ts"], 39 | "migrations": ["src/migration/**/*.ts"], 40 | "subscribers": ["src/subscriber/**/*.ts"], 41 | "cli": { 42 | "entitiesDir": "src/entity", 43 | "migrationsDir": "src/migration", 44 | "subscribersDir": "src/subscriber" 45 | } 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "postinstall": "./postinstall.sh", 5 | "build:server": "lerna run build --scope={@abb/common,@abb/server}", 6 | "build:web": "lerna run build --scope={@abb/common,@abb/controller,@abb/web}" 7 | }, 8 | "workspaces": { 9 | "packages": [ 10 | "packages/*" 11 | ], 12 | "nohoist": [ 13 | "**/rimraf", 14 | "**/rimraf/**", 15 | "**/react-native-elements", 16 | "**/react-native-elements/**", 17 | "**/react-native", 18 | "**/react-native/**", 19 | "**/expo", 20 | "**/expo/**", 21 | "**/react-native-typescript-transformer", 22 | "**/react-native-typescript-transformer/**", 23 | "**/metro-bundler-config-yarn-workspaces", 24 | "**/metro-bundler-config-yarn-workspaces/**" 25 | ] 26 | }, 27 | "devDependencies": { 28 | "lerna": "^2.11.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-expo"], 3 | "env": { 4 | "development": { 5 | "plugins": ["transform-react-jsx-source"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/app/App.tsx: -------------------------------------------------------------------------------- 1 | import App from "./src/index"; 2 | 3 | export default App; 4 | -------------------------------------------------------------------------------- /packages/app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "ts-app", 4 | "description": "This project is really great.", 5 | "slug": "ts-app", 6 | "privacy": "public", 7 | "sdkVersion": "27.0.0", 8 | "platforms": ["ios", "android"], 9 | "version": "1.0.0", 10 | "orientation": "portrait", 11 | "icon": "./assets/icon.png", 12 | "splash": { 13 | "image": "./assets/splash.png", 14 | "resizeMode": "contain", 15 | "backgroundColor": "#ffffff" 16 | }, 17 | "updates": { 18 | "fallbackToCacheTimeout": 0 19 | }, 20 | "assetBundlePatterns": ["**/*"], 21 | "ios": { 22 | "supportsTablet": true 23 | }, 24 | "ignoreNodeModulesValidation": true, 25 | "packagerOpts": { 26 | "config": "rn-cli.config.js", 27 | "projectRoots": "", 28 | "sourceExts": ["ts", "tsx"], 29 | "transformer": "node_modules/react-native-typescript-transformer/index.js" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/app/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benawad/fullstack-graphql-airbnb-clone/75efa273298c68aecc4c858c5671d48cc75b5e49/packages/app/assets/icon.png -------------------------------------------------------------------------------- /packages/app/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benawad/fullstack-graphql-airbnb-clone/75efa273298c68aecc4c858c5671d48cc75b5e49/packages/app/assets/splash.png -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@abb/app", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "private": true, 6 | "dependencies": { 7 | "@abb/common": "1.0.0", 8 | "@abb/controller": "1.0.0", 9 | "apollo-cache-inmemory": "^1.2.5", 10 | "apollo-client": "^2.3.5", 11 | "apollo-link-http": "^1.5.4", 12 | "apollo-upload-client": "^8.1.0", 13 | "expo": "^27.0.1", 14 | "formik": "^0.11.11", 15 | "graphql": "^0.13.2", 16 | "graphql-tag": "^2.9.2", 17 | "react": "16.3.1", 18 | "react-apollo": "^2.1.7", 19 | "react-native": "https://github.com/expo/react-native/archive/sdk-27.0.0.tar.gz", 20 | "react-native-elements": "1.0.0-beta5", 21 | "react-router-native": "^4.3.0" 22 | }, 23 | "devDependencies": { 24 | "@types/apollo-upload-client": "^8.1.0", 25 | "@types/expo": "^27.0.0", 26 | "@types/react": "^16.3.17", 27 | "@types/react-native": "^0.55.17", 28 | "@types/react-router-native": "^4.2.3", 29 | "exp": "^56.0.0", 30 | "metro-bundler-config-yarn-workspaces": "^1.0.3", 31 | "react-native-typescript-transformer": "^1.2.9", 32 | "typescript": "^3.0.3" 33 | }, 34 | "scripts": { 35 | "start": "exp start --offline", 36 | "ios": "exp ios", 37 | "android": "exp android" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/app/rn-cli.config.js: -------------------------------------------------------------------------------- 1 | const getConfig = require("metro-bundler-config-yarn-workspaces"); 2 | const path = require("path"); 3 | 4 | module.exports = getConfig(__dirname, { 5 | nodeModules: path.join(__dirname, "../..") 6 | }); 7 | -------------------------------------------------------------------------------- /packages/app/src/apollo.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient } from "apollo-client"; 2 | import { InMemoryCache } from "apollo-cache-inmemory"; 3 | import { createUploadLink } from "apollo-upload-client"; 4 | import { Platform } from "react-native"; 5 | 6 | const host = 7 | Platform.OS === "ios" ? "http://localhost:4000" : "http://10.0.2.2:4000"; 8 | 9 | export const client = new ApolloClient({ 10 | link: createUploadLink({ 11 | uri: host 12 | }), 13 | cache: new InMemoryCache() 14 | }); 15 | -------------------------------------------------------------------------------- /packages/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ApolloProvider } from "react-apollo"; 3 | import { Routes } from "./routes"; 4 | import { client } from "./apollo"; 5 | 6 | export default class App extends React.PureComponent { 7 | render() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/app/src/modules/listing/create/CreateListingConnector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Formik, Field, FormikActions } from "formik"; 3 | import { withCreateListing, WithCreateListing } from "@abb/controller"; 4 | import { RouteComponentProps } from "react-router-native"; 5 | import { Text, View, ScrollView } from "react-native"; 6 | import { Button } from "react-native-elements"; 7 | import { InputField } from "../../shared/InputField"; 8 | import { CheckboxGroupField } from "../../shared/CheckboxGroupField"; 9 | import { PictureField } from "../../shared/PictureField"; 10 | 11 | interface FormValues { 12 | picture: null; 13 | name: string; 14 | category: string; 15 | description: string; 16 | price: string; 17 | beds: string; 18 | guests: string; 19 | latitude: string; 20 | longitude: string; 21 | amenities: string[]; 22 | } 23 | 24 | class C extends React.PureComponent< 25 | RouteComponentProps<{}> & WithCreateListing 26 | > { 27 | submit = async ( 28 | { price, beds, guests, latitude, longitude, ...values }: FormValues, 29 | { setSubmitting }: FormikActions 30 | ) => { 31 | console.log(values); 32 | await this.props.createListing({ 33 | ...values, 34 | price: parseInt(price, 10), 35 | beds: parseInt(beds, 10), 36 | guests: parseInt(guests, 10), 37 | latitude: parseFloat(latitude), 38 | longitude: parseFloat(longitude) 39 | }); 40 | setSubmitting(false); 41 | }; 42 | 43 | render() { 44 | return ( 45 | 46 | initialValues={{ 47 | picture: null, 48 | name: "", 49 | category: "", 50 | description: "", 51 | price: "0", 52 | beds: "0", 53 | guests: "0", 54 | latitude: "0", 55 | longitude: "0", 56 | amenities: [] 57 | }} 58 | onSubmit={this.submit} 59 | > 60 | {({ handleSubmit, values }) => 61 | console.log(values) || ( 62 | 63 | 64 | Create Listing 65 | 66 | 67 | 72 | 77 | 82 | 89 | 96 | 103 | 110 | 117 | 122 | 48 | 49 | 50 | 51 | ); 52 | } 53 | } 54 | 55 | export const ChangePasswordView = withFormik({ 56 | validationSchema: changePasswordSchema, 57 | mapPropsToValues: () => ({ newPassword: "" }), 58 | handleSubmit: async ({ newPassword }, { props, setErrors }) => { 59 | const errors = await props.submit({ newPassword, key: props.token }); 60 | if (errors) { 61 | setErrors(errors); 62 | } else { 63 | props.onFinish(); 64 | } 65 | } 66 | })(C); 67 | -------------------------------------------------------------------------------- /packages/web/src/modules/forgotPassword/ForgotPasswordConnector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ForgotPasswordController } from "@abb/controller"; 3 | import { RouteComponentProps } from "react-router-dom"; 4 | 5 | import { ForgotPasswordView } from "./ui/ForgotPasswordView"; 6 | 7 | export class ForgotPasswordConnector extends React.PureComponent< 8 | RouteComponentProps<{}> 9 | > { 10 | onFinish = () => { 11 | this.props.history.push("/m/reset-password", { 12 | message: "check your email to reset your password" 13 | }); 14 | }; 15 | 16 | render() { 17 | return ( 18 | 19 | {({ submit }) => ( 20 | 21 | )} 22 | 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/web/src/modules/forgotPassword/ui/ForgotPasswordView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Form as AntForm, Icon, Button } from "antd"; 3 | import { withFormik, FormikProps, Field, Form } from "formik"; 4 | import { NormalizedErrorMap } from "@abb/controller"; 5 | 6 | import { InputField } from "../../shared/InputField"; 7 | 8 | const FormItem = AntForm.Item; 9 | 10 | interface FormValues { 11 | email: string; 12 | } 13 | 14 | interface Props { 15 | onFinish: () => void; 16 | submit: (values: FormValues) => Promise; 17 | } 18 | 19 | class C extends React.PureComponent & Props> { 20 | render() { 21 | return ( 22 |
23 |
24 | as any 28 | } 29 | placeholder="Email" 30 | component={InputField} 31 | /> 32 | 33 | 40 | 41 |
42 |
43 | ); 44 | } 45 | } 46 | 47 | export const ForgotPasswordView = withFormik({ 48 | mapPropsToValues: () => ({ email: "" }), 49 | handleSubmit: async (values, { props, setErrors }) => { 50 | const errors = await props.submit(values); 51 | if (errors) { 52 | setErrors(errors); 53 | } else { 54 | props.onFinish(); 55 | } 56 | } 57 | })(C); 58 | -------------------------------------------------------------------------------- /packages/web/src/modules/listing/create/CreateListingConnector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { RouteComponentProps } from "react-router-dom"; 3 | import { FormikActions } from "formik"; 4 | import { withCreateListing, WithCreateListing } from "@abb/controller"; 5 | import { ListingFormValues, ListingForm } from "../shared/ListingForm"; 6 | 7 | class C extends React.PureComponent< 8 | RouteComponentProps<{}> & WithCreateListing 9 | > { 10 | submit = async ( 11 | values: ListingFormValues, 12 | { setSubmitting }: FormikActions 13 | ) => { 14 | await this.props.createListing(values); 15 | setSubmitting(false); 16 | }; 17 | 18 | render() { 19 | return ; 20 | } 21 | } 22 | 23 | export const CreateListingConnector = withCreateListing(C); 24 | -------------------------------------------------------------------------------- /packages/web/src/modules/listing/delete/DemoDelete.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Mutation } from "react-apollo"; 3 | import gql from "graphql-tag"; 4 | 5 | export class DemoDelete extends React.PureComponent { 6 | render() { 7 | return ( 8 | 15 | {mutate => } 16 | 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/web/src/modules/listing/edit/EditListingConnector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ViewListing, UpdateListing } from "@abb/controller"; 3 | import { RouteComponentProps } from "react-router-dom"; 4 | import { ListingForm, defaultListingFormValues } from "../shared/ListingForm"; 5 | 6 | export class EditListingConnector extends React.PureComponent< 7 | RouteComponentProps<{ 8 | listingId: string; 9 | }> 10 | > { 11 | render() { 12 | const { 13 | match: { 14 | params: { listingId } 15 | } 16 | } = this.props; 17 | return ( 18 | 19 | {data => { 20 | console.log(data); 21 | if (!data.listing) { 22 | return
...loading
; 23 | } 24 | 25 | const { id: _, owner: ___, ...listing } = data.listing; 26 | 27 | return ( 28 | 29 | {({ updateListing }) => ( 30 | { 36 | const { __typename: ____, ...newValues } = values as any; 37 | 38 | if (newValues.pictureUrl) { 39 | const parts = newValues.pictureUrl.split("/"); 40 | newValues.pictureUrl = parts[parts.length - 1]; 41 | } 42 | 43 | const result = await updateListing({ 44 | variables: { 45 | input: newValues, 46 | listingId 47 | } 48 | }); 49 | 50 | console.log(result); 51 | }} 52 | /> 53 | )} 54 | 55 | ); 56 | }} 57 |
58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/web/src/modules/listing/find/FindListingsConnector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Card } from "antd"; 3 | import { withFindListings, WithFindListings } from "@abb/controller"; 4 | import { Link } from "react-router-dom"; 5 | 6 | const { Meta } = Card; 7 | 8 | class C extends React.PureComponent { 9 | render() { 10 | const { listings, loading } = this.props; 11 | return ( 12 |
13 | {loading &&
...loading
} 14 | {listings.map(l => ( 15 | } 20 | > 21 | 22 | 23 | 24 | 25 | ))} 26 |
27 | ); 28 | } 29 | } 30 | 31 | export const FindListingsConnector = withFindListings(C); 32 | -------------------------------------------------------------------------------- /packages/web/src/modules/listing/messages/InputBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Formik, Form, Field } from "formik"; 3 | import { CreateMessage } from "@abb/controller"; 4 | import { InputField } from "../../shared/InputField"; 5 | 6 | interface FormValues { 7 | text: string; 8 | } 9 | 10 | interface Props { 11 | listingId: string; 12 | } 13 | 14 | export class InputBar extends React.PureComponent { 15 | render() { 16 | const { listingId } = this.props; 17 | return ( 18 | 19 | {({ createMessage }) => ( 20 | 21 | initialValues={{ text: "" }} 22 | onSubmit={async ({ text }, { resetForm }) => { 23 | await createMessage({ 24 | variables: { 25 | message: { 26 | text, 27 | listingId 28 | } 29 | } 30 | }); 31 | resetForm(); 32 | }} 33 | > 34 | {() => ( 35 |
36 | 37 | 38 | 39 | )} 40 | 41 | )} 42 |
43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/web/src/modules/listing/messages/MessageConnector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { RouteComponentProps } from "react-router-dom"; 3 | import { ViewMessages } from "@abb/controller"; 4 | import { InputBar } from "./InputBar"; 5 | 6 | export class MessageConnector extends React.PureComponent< 7 | RouteComponentProps<{ 8 | listingId: string; 9 | }> 10 | > { 11 | unsubscribe: () => void; 12 | 13 | render() { 14 | const { 15 | match: { 16 | params: { listingId } 17 | } 18 | } = this.props; 19 | return ( 20 | 21 | {({ loading, messages, subscribe }) => { 22 | if (loading) { 23 | return
...loading
; 24 | } 25 | 26 | if (!this.unsubscribe) { 27 | this.unsubscribe = subscribe(); 28 | } 29 | 30 | return ( 31 |
32 | {messages.map((m, i) => ( 33 |
{m.text}
34 | ))} 35 | 36 | 37 |
38 | ); 39 | }} 40 |
41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/web/src/modules/listing/shared/ListingForm.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { Form as AntForm, Button } from "antd"; 4 | import { Form, Formik, FormikActions } from "formik"; 5 | import { ImageFile } from "react-dropzone"; 6 | 7 | import { Page1 } from "./ui/Page1"; 8 | import { Page2 } from "./ui/Page2"; 9 | import { Page3 } from "./ui/Page3"; 10 | 11 | const FormItem = AntForm.Item; 12 | 13 | export interface ListingFormValues { 14 | pictureUrl: string | null; 15 | picture: ImageFile | null; 16 | name: string; 17 | category: string; 18 | description: string; 19 | price: number; 20 | beds: number; 21 | guests: number; 22 | latitude: number; 23 | longitude: number; 24 | amenities: string[]; 25 | } 26 | 27 | interface State { 28 | page: number; 29 | } 30 | 31 | interface Props { 32 | initialValues?: ListingFormValues; 33 | submit: ( 34 | data: ListingFormValues, 35 | actions: FormikActions 36 | ) => Promise; 37 | } 38 | 39 | // tslint:disable-next-line:jsx-key 40 | const pages = [, , ]; 41 | 42 | export const defaultListingFormValues = { 43 | pictureUrl: null, 44 | picture: null, 45 | name: "", 46 | category: "", 47 | description: "", 48 | price: 0, 49 | beds: 0, 50 | guests: 0, 51 | latitude: 0, 52 | longitude: 0, 53 | amenities: [] 54 | }; 55 | 56 | export class ListingForm extends React.PureComponent { 57 | state = { 58 | page: 0 59 | }; 60 | 61 | nextPage = () => this.setState(state => ({ page: state.page + 1 })); 62 | 63 | render() { 64 | const { submit, initialValues = defaultListingFormValues } = this.props; 65 | 66 | return ( 67 | 68 | initialValues={initialValues} 69 | onSubmit={submit} 70 | > 71 | {({ isSubmitting, values }) => 72 | ( 73 |
74 | logout 75 |
76 | {pages[this.state.page]} 77 | 78 |
84 | {this.state.page === pages.length - 1 ? ( 85 |
86 | 93 |
94 | ) : ( 95 | 98 | )} 99 |
100 |
101 |
102 |
103 | ) 104 | } 105 | 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /packages/web/src/modules/listing/shared/ui/Page1.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Field } from "formik"; 3 | 4 | import { InputField } from "../../../../modules/shared/InputField"; 5 | import { DropzoneField } from "../../../shared/DropzoneField"; 6 | 7 | export const Page1 = () => ( 8 | <> 9 | 10 | 11 | 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /packages/web/src/modules/listing/shared/ui/Page2.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Field } from "formik"; 3 | 4 | import { InputField } from "../../../../modules/shared/InputField"; 5 | 6 | export const Page2 = () => ( 7 | <> 8 | 15 | 22 | 29 | 30 | ); 31 | -------------------------------------------------------------------------------- /packages/web/src/modules/listing/shared/ui/Page3.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Field } from "formik"; 3 | 4 | import { TagField } from "../../../shared/TagField"; 5 | import { LocationField } from "../../../shared/LocationField"; 6 | 7 | export const Page3 = () => ( 8 | <> 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /packages/web/src/modules/listing/view/ViewListingConnector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ViewListing } from "@abb/controller"; 3 | import { RouteComponentProps, Link } from "react-router-dom"; 4 | 5 | export class ViewListingConnector extends React.PureComponent< 6 | RouteComponentProps<{ 7 | listingId: string; 8 | }> 9 | > { 10 | render() { 11 | const { 12 | match: { 13 | params: { listingId } 14 | } 15 | } = this.props; 16 | return ( 17 | 18 | {data => { 19 | console.log(data); 20 | if (!data.listing) { 21 | return
...loading
; 22 | } 23 | 24 | return ( 25 |
26 |
{data.listing.name}
27 |
28 | chat 29 |
30 |
31 | edit 32 |
33 |
34 | ); 35 | }} 36 |
37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/web/src/modules/login/LoginConnector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { LoginController } from "@abb/controller"; 3 | import { RouteComponentProps } from "react-router-dom"; 4 | 5 | import { LoginView } from "./ui/LoginView"; 6 | 7 | export class LoginConnector extends React.PureComponent< 8 | RouteComponentProps<{}> 9 | > { 10 | onFinish = () => { 11 | const { 12 | history, 13 | location: { state } 14 | } = this.props; 15 | if (state && state.next) { 16 | return history.push(state.next); 17 | } 18 | 19 | history.push("/"); 20 | }; 21 | 22 | render() { 23 | console.log(this.props.location.state); 24 | 25 | return ( 26 | 27 | {({ submit }) => } 28 | 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/web/src/modules/login/ui/LoginView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Form as AntForm, Icon, Button } from "antd"; 3 | import { withFormik, FormikProps, Field, Form } from "formik"; 4 | import { loginSchema } from "@abb/common"; 5 | import { Link } from "react-router-dom"; 6 | import { NormalizedErrorMap } from "@abb/controller"; 7 | 8 | import { InputField } from "../../shared/InputField"; 9 | 10 | const FormItem = AntForm.Item; 11 | 12 | interface FormValues { 13 | email: string; 14 | password: string; 15 | } 16 | 17 | interface Props { 18 | onFinish: () => void; 19 | submit: (values: FormValues) => Promise; 20 | } 21 | 22 | class C extends React.PureComponent & Props> { 23 | render() { 24 | return ( 25 |
26 |
27 | as any 31 | } 32 | placeholder="Email" 33 | component={InputField} 34 | /> 35 | as any 40 | } 41 | placeholder="Password" 42 | component={InputField} 43 | /> 44 | 45 | Forgot password 46 | 47 | 48 | 55 | 56 | 57 | Or register 58 | 59 |
60 |
61 | ); 62 | } 63 | } 64 | 65 | export const LoginView = withFormik({ 66 | validationSchema: loginSchema, 67 | validateOnChange: false, 68 | validateOnBlur: false, 69 | mapPropsToValues: () => ({ email: "", password: "" }), 70 | handleSubmit: async (values, { props, setErrors }) => { 71 | const errors = await props.submit(values); 72 | if (errors) { 73 | setErrors(errors); 74 | } else { 75 | props.onFinish(); 76 | } 77 | } 78 | })(C); 79 | -------------------------------------------------------------------------------- /packages/web/src/modules/logout/CallLogout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface Props { 4 | logout: () => void; 5 | onFinish: () => void; 6 | } 7 | 8 | export class CallLogout extends React.PureComponent { 9 | async componentDidMount() { 10 | await this.props.logout(); 11 | this.props.onFinish(); 12 | } 13 | 14 | render() { 15 | return null; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/web/src/modules/logout/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { LogoutController } from "@abb/controller"; 3 | import { RouteComponentProps } from "react-router-dom"; 4 | 5 | import { CallLogout } from "./CallLogout"; 6 | 7 | export class Logout extends React.PureComponent> { 8 | onFinish = () => { 9 | this.props.history.push("/login"); 10 | }; 11 | 12 | render() { 13 | return ( 14 | 15 | {({ logout }) => ( 16 | 17 | )} 18 | 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/web/src/modules/register/RegisterConnector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { RegisterController } from "@abb/controller"; 3 | import { RouteComponentProps } from "react-router-dom"; 4 | 5 | import { RegisterView } from "./ui/RegisterView"; 6 | 7 | // container -> view 8 | // container -> connector -> view 9 | // controller -> connector -> view 10 | 11 | export class RegisterConnector extends React.PureComponent< 12 | RouteComponentProps<{}> 13 | > { 14 | onFinish = () => { 15 | this.props.history.push("/m/confirm-email", { 16 | message: "check your email to confirm your account" 17 | }); 18 | }; 19 | 20 | render() { 21 | return ( 22 | 23 | {({ submit }) => ( 24 | 25 | )} 26 | 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/web/src/modules/register/ui/RegisterView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Form as AntForm, Icon, Button } from "antd"; 3 | import { withFormik, FormikProps, Field, Form } from "formik"; 4 | import { validUserSchema } from "@abb/common"; 5 | import { InputField } from "../../shared/InputField"; 6 | import { Link } from "react-router-dom"; 7 | import { NormalizedErrorMap } from "@abb/controller"; 8 | 9 | const FormItem = AntForm.Item; 10 | 11 | interface FormValues { 12 | email: string; 13 | password: string; 14 | } 15 | 16 | interface Props { 17 | onFinish: () => void; 18 | submit: (values: FormValues) => Promise; 19 | } 20 | 21 | class C extends React.PureComponent & Props> { 22 | render() { 23 | return ( 24 |
25 |
26 | as any 30 | } 31 | placeholder="Email" 32 | component={InputField} 33 | /> 34 | as any 39 | } 40 | placeholder="Password" 41 | component={InputField} 42 | /> 43 | 44 | Forgot password 45 | 46 | 47 | 54 | 55 | 56 | Or login now! 57 | 58 |
59 |
60 | ); 61 | } 62 | } 63 | 64 | export const RegisterView = withFormik({ 65 | validationSchema: validUserSchema, 66 | mapPropsToValues: () => ({ email: "", password: "" }), 67 | handleSubmit: async (values, { props, setErrors }) => { 68 | const errors = await props.submit(values); 69 | if (errors) { 70 | setErrors(errors); 71 | } else { 72 | props.onFinish(); 73 | } 74 | } 75 | })(C); 76 | -------------------------------------------------------------------------------- /packages/web/src/modules/shared/DropzoneField.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { FieldProps } from "formik"; 3 | import Dropzone from "react-dropzone"; 4 | import { Button } from "antd"; 5 | 6 | export const DropzoneField: React.SFC> = ({ 7 | field: { name, value }, 8 | form: { setFieldValue, values, setValues }, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc. 9 | ...props 10 | }) => { 11 | const pUrl = (value ? value.preview : null) || values.pictureUrl; 12 | return ( 13 |
14 | { 18 | setFieldValue(name, file); 19 | }} 20 | {...props} 21 | > 22 |

Try dropping some files here, or click to select files to upload.

23 |
24 | {pUrl && ( 25 | 31 | )} 32 | 43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/web/src/modules/shared/InputField.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { FieldProps } from "formik"; 3 | import { Form, Input, InputNumber } from "antd"; 4 | 5 | const FormItem = Form.Item; 6 | 7 | export const InputField: React.SFC< 8 | FieldProps & { 9 | prefix: React.ReactNode; 10 | label?: string; 11 | useNumberComponent?: boolean; 12 | } 13 | > = ({ 14 | field: { onChange, ...field }, 15 | form: { touched, errors, setFieldValue }, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc. 16 | label, 17 | useNumberComponent = false, 18 | ...props 19 | }) => { 20 | const errorMsg = touched[field.name] && errors[field.name]; 21 | 22 | const Comp = useNumberComponent ? InputNumber : Input; 23 | 24 | return ( 25 | 30 | setFieldValue(field.name, newValue) 36 | : onChange 37 | } 38 | /> 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/web/src/modules/shared/LocationField.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { FieldProps } from "formik"; 3 | import Geosuggest, { Suggest } from "react-geosuggest"; 4 | import { withGoogleMap, GoogleMap, Marker } from "react-google-maps"; 5 | 6 | import "./geo.css"; 7 | 8 | interface DefaultCenter { 9 | lat: number; 10 | lng: number; 11 | } 12 | 13 | // -34.397, lng: 150.644 14 | const MapWithAMarker = withGoogleMap<{ 15 | defaultCenter: DefaultCenter; 16 | lat: number; 17 | lng: number; 18 | onClick: (e: google.maps.KmlMouseEvent | google.maps.MouseEvent) => void; 19 | }>(props => ( 20 | 25 | 26 | 27 | )); 28 | 29 | interface State { 30 | defaultCenter: DefaultCenter | null; 31 | } 32 | 33 | export class LocationField extends React.PureComponent< 34 | FieldProps & {}, 35 | State 36 | > { 37 | state: State = { 38 | defaultCenter: null 39 | }; 40 | 41 | onSuggestSelect = (place: Suggest) => { 42 | if (!place) { 43 | return; 44 | } 45 | 46 | const { 47 | location: { lat, lng } 48 | } = place; 49 | const { 50 | form: { setValues, values } 51 | } = this.props; 52 | setValues({ 53 | ...values, 54 | latitude: lat, 55 | longitude: lng 56 | }); 57 | 58 | this.setState({ 59 | defaultCenter: { 60 | lat: parseFloat(lat), 61 | lng: parseFloat(lng) 62 | } 63 | }); 64 | }; 65 | 66 | render() { 67 | const { 68 | form: { values, setValues } // also values, setXXXX, handleXXXX, dirty, isValid, status, etc. 69 | } = this.props; 70 | 71 | return ( 72 |
73 | 79 |
{values.longitude}
80 |
{values.latitude}
81 | 82 | {this.state.defaultCenter && ( 83 | } 85 | mapElement={
} 86 | defaultCenter={this.state.defaultCenter} 87 | lat={values.latitude} 88 | lng={values.longitude} 89 | onClick={x => { 90 | const lat = x.latLng.lat(); 91 | const lng = x.latLng.lng(); 92 | 93 | setValues({ 94 | ...values, 95 | latitude: lat, 96 | longitude: lng 97 | }); 98 | }} 99 | /> 100 | )} 101 |
102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/web/src/modules/shared/TagField.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { FieldProps } from "formik"; 3 | import { Form, Select } from "antd"; 4 | 5 | const FormItem = Form.Item; 6 | 7 | export const TagField: React.SFC< 8 | FieldProps & { 9 | prefix: React.ReactNode; 10 | label?: string; 11 | } 12 | > = ({ 13 | field: { onChange, onBlur: _, ...field }, 14 | form: { touched, errors, setFieldValue }, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc. 15 | label, 16 | ...props 17 | }) => { 18 | const errorMsg = touched[field.name] && errors[field.name]; 19 | 20 | return ( 21 | 26 |