├── .github
├── dependabot.yml
└── workflows
│ ├── app1-build-and-deploy-client.yml.disabled
│ ├── app1-build-and-deploy-server.yml.disabled
│ ├── app2-build-and-deploy-client.yml.disabled
│ ├── app2-build-and-deploy-server.yml.disabled
│ └── app2-sonar-cloud-scan.yml.disabled
├── .gitignore
├── .prettierignore
├── .prettierrc
├── Readme.md
├── app1
├── Readme-postgresql.md
├── Readme.md
├── app
│ ├── client
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── Dockerfile.dev
│ │ ├── Dockerfile.prod
│ │ ├── nginx.conf
│ │ ├── package.json
│ │ ├── public
│ │ │ ├── favicon.ico
│ │ │ ├── index.html
│ │ │ └── manifest.json
│ │ ├── schema.json
│ │ ├── src
│ │ │ ├── index.tsx
│ │ │ ├── react-app-env.d.ts
│ │ │ ├── sections
│ │ │ │ ├── Listings
│ │ │ │ │ ├── Listings.tsx
│ │ │ │ │ ├── __generated__
│ │ │ │ │ │ ├── DeleteListing.ts
│ │ │ │ │ │ └── Listings.ts
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── ListingsSkeleton
│ │ │ │ │ │ │ ├── ListingsSkeleton.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── styles
│ │ │ │ │ │ │ │ └── ListingsSkeleton.css
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── styles
│ │ │ │ │ │ └── Listings.css
│ │ │ │ └── index.ts
│ │ │ └── styles
│ │ │ │ └── index.css
│ │ └── tsconfig.json
│ └── server
│ │ ├── mongodb
│ │ ├── .dockerignore
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── Dockerfile
│ │ ├── package.json
│ │ ├── seed
│ │ │ └── seed.ts
│ │ ├── src
│ │ │ ├── database
│ │ │ │ └── index.ts
│ │ │ ├── graphql
│ │ │ │ ├── index.ts
│ │ │ │ ├── resolvers
│ │ │ │ │ ├── Listing
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── index.ts
│ │ │ │ └── typeDefs.ts
│ │ │ ├── index.ts
│ │ │ └── lib
│ │ │ │ └── types.ts
│ │ └── tsconfig.json
│ │ └── postgresql
│ │ ├── .dockerignore
│ │ ├── .env
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── Dockerfile
│ │ ├── docker-compose.yml
│ │ ├── ormconfig.json
│ │ ├── package.json
│ │ ├── seed
│ │ └── seed.ts
│ │ ├── src
│ │ ├── database
│ │ │ ├── entity
│ │ │ │ ├── ListingEntity.ts
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── graphql
│ │ │ ├── index.ts
│ │ │ ├── resolvers
│ │ │ │ ├── Listing
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ └── typeDefs.ts
│ │ ├── index.ts
│ │ └── lib
│ │ │ └── types.ts
│ │ └── tsconfig.json
├── k8s
│ ├── client-clusterIP.yaml
│ ├── client-deployment.yaml
│ ├── ingress-config.yaml
│ ├── mongo-clusterIP.yaml
│ ├── mongo-deployment.yaml
│ ├── server-clusterIP.yaml
│ └── server-deployment.yaml
└── skaffold
│ └── skaffold.yaml
├── app2
├── Development.md
├── Readme-postgresql.md
├── Readme.md
├── app
│ ├── client
│ │ ├── .dockerignore
│ │ ├── .env
│ │ ├── .gitignore
│ │ ├── Dockerfile.dev
│ │ ├── Dockerfile.prod
│ │ ├── apollo.config.js
│ │ ├── nginx.conf
│ │ ├── package.json
│ │ ├── public
│ │ │ ├── favicon.ico
│ │ │ ├── index.html
│ │ │ └── manifest.json
│ │ ├── schema.json
│ │ ├── src
│ │ │ ├── index.tsx
│ │ │ ├── lib
│ │ │ │ ├── components
│ │ │ │ │ ├── AppHeaderSkeleton
│ │ │ │ │ │ ├── assets
│ │ │ │ │ │ │ └── tinyhouse-logo.png
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── ErrorBanner
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── ListingCard
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── PageSkeleton
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── graphql
│ │ │ │ │ ├── globalTypes.ts
│ │ │ │ │ ├── mutations
│ │ │ │ │ │ ├── ConnectStripe
│ │ │ │ │ │ │ ├── __generated__
│ │ │ │ │ │ │ │ └── ConnectStripe.ts
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── CreateBooking
│ │ │ │ │ │ │ ├── __generated__
│ │ │ │ │ │ │ │ └── CreateBooking.ts
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── DisconnectStripe
│ │ │ │ │ │ │ ├── __generated__
│ │ │ │ │ │ │ │ └── DisconnectStripe.ts
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── HostListing
│ │ │ │ │ │ │ ├── __generated__
│ │ │ │ │ │ │ │ └── HostListing.ts
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── LogIn
│ │ │ │ │ │ │ ├── __generated__
│ │ │ │ │ │ │ │ └── LogIn.ts
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── LogOut
│ │ │ │ │ │ │ ├── __generated__
│ │ │ │ │ │ │ │ └── LogOut.ts
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── queries
│ │ │ │ │ │ ├── AuthUrl
│ │ │ │ │ │ ├── __generated__
│ │ │ │ │ │ │ └── AuthUrl.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── Listing
│ │ │ │ │ │ ├── __generated__
│ │ │ │ │ │ │ └── Listing.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── Listings
│ │ │ │ │ │ ├── __generated__
│ │ │ │ │ │ │ └── Listings.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── User
│ │ │ │ │ │ ├── __generated__
│ │ │ │ │ │ │ └── User.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ ├── hooks
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── useScrollToTop
│ │ │ │ │ │ └── index.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ │ └── index.ts
│ │ │ ├── react-app-env.d.ts
│ │ │ ├── sections
│ │ │ │ ├── AppHeader
│ │ │ │ │ ├── assets
│ │ │ │ │ │ └── tinyhouse-logo.png
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── MenuItems
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Home
│ │ │ │ │ ├── assets
│ │ │ │ │ │ ├── cancun.jpg
│ │ │ │ │ │ ├── dubai.jpg
│ │ │ │ │ │ ├── listing-loading-card-cover.jpg
│ │ │ │ │ │ ├── london.jpg
│ │ │ │ │ │ ├── los-angeles.jpg
│ │ │ │ │ │ ├── map-background.jpg
│ │ │ │ │ │ ├── san-fransisco.jpg
│ │ │ │ │ │ └── toronto.jpg
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── HomeHero
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── HomeListings
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── HomeListingsSkeleton
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Host
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Listing
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── ListingBookings
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── ListingCreateBooking
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── ListingCreateBookingModal
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── ListingDetails
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Listings
│ │ │ │ │ ├── assets
│ │ │ │ │ │ └── listing-loading-card-cover.jpg
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── ListingsFilters
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── ListingsPagination
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── ListingsSkeleton
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Login
│ │ │ │ │ ├── assets
│ │ │ │ │ │ └── google_logo.jpg
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── NotFound
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Stripe
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── User
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── UserBookings
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── UserListings
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── UserProfile
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── index.tsx
│ │ │ │ └── index.ts
│ │ │ └── styles
│ │ │ │ └── index.css
│ │ ├── tsconfig.json
│ │ └── yarn.lock
│ └── server
│ │ ├── mondodb
│ │ ├── .dockerignore
│ │ ├── .env
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── Dockerfile.dev
│ │ ├── docker-compose.yml
│ │ ├── package.json
│ │ ├── seed
│ │ │ ├── clear.ts
│ │ │ └── seed.ts
│ │ ├── src
│ │ │ ├── database
│ │ │ │ └── index.ts
│ │ │ ├── graphql
│ │ │ │ ├── index.ts
│ │ │ │ ├── resolvers
│ │ │ │ │ ├── Booking
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── Listing
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── User
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── Viewer
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ └── index.ts
│ │ │ │ └── typeDefs.ts
│ │ │ ├── index.ts
│ │ │ └── lib
│ │ │ │ ├── api
│ │ │ │ ├── Cloudinary.ts
│ │ │ │ ├── Google.ts
│ │ │ │ ├── Stripe.ts
│ │ │ │ └── index.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── index.ts
│ │ └── tsconfig.json
│ │ └── postgresql
│ │ ├── .dockerignore
│ │ ├── .env
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── Dockerfile.dev
│ │ ├── docker-compose.yml
│ │ ├── ormconfig.json
│ │ ├── package.json
│ │ ├── seed
│ │ ├── clear.ts
│ │ └── seed.ts
│ │ ├── src
│ │ ├── database
│ │ │ ├── entity
│ │ │ │ ├── BookingEntity.ts
│ │ │ │ ├── ListingEntity.ts
│ │ │ │ ├── UserEntity.ts
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── graphql
│ │ │ ├── index.ts
│ │ │ ├── resolvers
│ │ │ │ ├── Booking
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── Listing
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── User
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── Viewer
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── types.ts
│ │ │ │ └── index.ts
│ │ │ └── typeDefs.ts
│ │ ├── index.ts
│ │ └── lib
│ │ │ ├── api
│ │ │ ├── Cloudinary.ts
│ │ │ ├── Google.ts
│ │ │ ├── Stripe.ts
│ │ │ └── index.ts
│ │ │ ├── types.ts
│ │ │ └── utils
│ │ │ └── index.ts
│ │ ├── tsconfig.json
│ │ └── yarn.lock
├── k8s
│ ├── client-clusterIP.yaml
│ ├── client-deployment.yaml
│ ├── ingress-config.yaml
│ ├── mongo-clusterIP.yaml
│ ├── mongo-deployment.yaml
│ ├── server-clusterIP.yaml
│ └── server-deployment.yaml
└── skaffold
│ └── skaffold.yaml
├── img
├── pic-app-1-final.png
├── pic-app-2-current.png
├── pic-m08-p01.png
├── pic-m08-p02.png
├── pic-m09-p01.png
├── pic-m09-p02.png
├── pic-m09-p03.png
├── pic-m09-p04.png
├── pic-m09-p05.png
├── pic-m09-p06.png
├── pic-m09-p07.png
├── pic-m09-p08.png
├── pic-m09-p09.png
├── pic-m10-p01.png
├── pic-m10-p02.png
├── pic-m10-p03.png
├── pic-m10-p04.png
├── pic-m13-p01.png
├── pic-setup-k9s-01.png
├── pic-setup-k9s-02.png
├── pic19.png
├── pic20.png
├── pic21.png
├── pic22.png
├── pic23.png
├── pic24.png
├── pic35-1.png
├── pic35-2.png
├── pic36-1.png
├── pic42.png
└── pic43.png
└── sonar-project.properties
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: 'github-actions'
4 | directory: '/'
5 | schedule:
6 | interval: 'monthly'
7 | - package-ecosystem: 'npm'
8 | directory: '/app1/app/client/'
9 | schedule:
10 | interval: 'monthly'
11 | - package-ecosystem: 'npm'
12 | directory: '/app1/app/server/mongodb/'
13 | schedule:
14 | interval: 'monthly'
15 | - package-ecosystem: 'npm'
16 | directory: '/app1/app/server/postgresql/'
17 | schedule:
18 | interval: 'monthly'
19 | - package-ecosystem: 'docker'
20 | directory: '/app1/app/client/'
21 | schedule:
22 | interval: 'monthly'
23 | - package-ecosystem: 'docker'
24 | directory: '/app1/app/server/mongodb/'
25 | schedule:
26 | interval: 'monthly'
27 | - package-ecosystem: 'docker'
28 | directory: '/app1/app/server/postgresql/'
29 | schedule:
30 | interval: 'monthly'
31 | - package-ecosystem: 'npm'
32 | directory: '/app2/app/client/'
33 | schedule:
34 | interval: 'monthly'
35 | - package-ecosystem: 'npm'
36 | directory: '/app2/app/server/'
37 | schedule:
38 | interval: 'monthly'
39 | - package-ecosystem: 'docker'
40 | directory: '/app2/app/client/'
41 | schedule:
42 | interval: 'monthly'
43 | - package-ecosystem: 'docker'
44 | directory: '/app2/app/server/'
45 | schedule:
46 | interval: 'monthly'
47 |
--------------------------------------------------------------------------------
/.github/workflows/app1-build-and-deploy-client.yml.disabled:
--------------------------------------------------------------------------------
1 | name: Build and Deploy Client
2 | on:
3 | pull_request:
4 | branches:
5 | - master
6 | push:
7 | branches:
8 | - master
9 | paths:
10 | - 'app1/app/client/**'
11 |
12 | jobs:
13 | build:
14 | name: Build Client
15 | runs-on: ubuntu-20.04
16 | steps:
17 | - uses: actions/checkout@master
18 |
19 | # DOCKERHUB
20 |
21 | - name: Build the Docker container image
22 | run: docker build ./app1/app/client -f ./app1/app/client/Dockerfile.prod -t webmakaka/tinyhouse-client-app1:latest
23 | - name: Push the image to hub.docker.com
24 | run: |
25 | docker login -u webmakaka -p "${DOCKER_HUB_PASS}"
26 | docker push webmakaka/tinyhouse-client-app1:latest
27 | env:
28 | DOCKER_HUB_PASS: ${{ secrets.DOCKER_HUB_PASS }}
29 |
--------------------------------------------------------------------------------
/.github/workflows/app1-build-and-deploy-server.yml.disabled:
--------------------------------------------------------------------------------
1 | name: Build and Deploy Server
2 | on:
3 | pull_request:
4 | branches:
5 | - master
6 | push:
7 | branches:
8 | - master
9 | paths:
10 | - 'app1/app/server/mongodb/**'
11 |
12 | jobs:
13 | build:
14 | name: Build Server
15 | runs-on: ubuntu-20.04
16 | steps:
17 | - uses: actions/checkout@master
18 |
19 | # DOCKERHUB
20 |
21 | - name: Build the Docker container image
22 | run: docker build ./app1/app/server/mongodb -f ./app1/app/server/mongodb/Dockerfile -t webmakaka/tinyhouse-server-app1:latest
23 |
24 | - name: Push the image to hub.docker.com
25 | run: |
26 | docker login -u webmakaka -p "${DOCKER_HUB_PASS}"
27 | docker push webmakaka/tinyhouse-server-app1:latest
28 | env:
29 | DOCKER_HUB_PASS: ${{ secrets.DOCKER_HUB_PASS }}
30 |
--------------------------------------------------------------------------------
/.github/workflows/app2-build-and-deploy-client.yml.disabled:
--------------------------------------------------------------------------------
1 | name: Build and Deploy Client
2 | on:
3 | pull_request:
4 | branches:
5 | - master
6 | push:
7 | branches:
8 | - master
9 | paths:
10 | - 'app2/app/client/**'
11 |
12 | jobs:
13 | build:
14 | name: Build Client
15 | runs-on: ubuntu-20.04
16 | steps:
17 | - uses: actions/checkout@master
18 |
19 | # DOCKERHUB
20 |
21 | - name: Build the Docker container image
22 | run: docker build ./app2/app/client -f ./app2/app/client/Dockerfile.prod -t webmakaka/tinyhouse-client:latest
23 | - name: Push the image to hub.docker.com
24 | run: |
25 | docker login -u webmakaka -p "${DOCKER_HUB_PASS}"
26 | docker push webmakaka/tinyhouse-client:latest
27 | env:
28 | DOCKER_HUB_PASS: ${{ secrets.DOCKER_HUB_PASS }}
29 | # Github Package Registry (GPR)
30 |
31 | # - name: Build the Docker container image
32 | # run: docker build ./client -f ./client/Dockerfile -t docker.pkg.github.com/marley-nodejs/mern-stack-front-to-back-v2.0/client.anketa.info:latest
33 |
34 | # - name: Push the image to Github Package Registry (GPR)
35 | # run: |
36 | # docker login docker.pkg.github.com -u publisher -p "${GITHUB_PACKAGE_REGISTRY_TOKEN}"
37 | # docker push docker.pkg.github.com/marley-nodejs/mern-stack-front-to-back-v2.0/client.anketa.info:latest
38 | # env:
39 | # GITHUB_PACKAGE_REGISTRY_TOKEN: ${{ secrets.GITHUB_PACKAGE_REGISTRY_TOKEN }}
40 |
--------------------------------------------------------------------------------
/.github/workflows/app2-build-and-deploy-server.yml.disabled:
--------------------------------------------------------------------------------
1 | name: Build and Deploy Server
2 | on:
3 | pull_request:
4 | branches:
5 | - master
6 | push:
7 | branches:
8 | - master
9 | paths:
10 | - 'app2/app/server/**'
11 |
12 | jobs:
13 | build:
14 | name: Build Server
15 | runs-on: ubuntu-20.04
16 | steps:
17 | - uses: actions/checkout@master
18 |
19 | # DOCKERHUB
20 |
21 | - name: Build the Docker container image
22 | run: docker build ./app2/app/server -f ./app2/app/server/Dockerfile -t webmakaka/tinyhouse-server:latest
23 |
24 | - name: Push the image to hub.docker.com
25 | run: |
26 | docker login -u webmakaka -p "${DOCKER_HUB_PASS}"
27 | docker push webmakaka/tinyhouse-server:latest
28 | env:
29 | DOCKER_HUB_PASS: ${{ secrets.DOCKER_HUB_PASS }}
30 | # Github Package Registry (GPR)
31 |
32 | # - name: Build the Docker container image
33 | # run: docker build ./api -f ./api/Dockerfile -t docker.pkg.github.com/marley-nodejs/mern-stack-front-to-back-v2.0/api.anketa.info:latest
34 |
35 | # - name: Push the image to Github Package Registry (GPR)
36 | # run: |
37 | # docker login docker.pkg.github.com -u publisher -p "${GITHUB_PACKAGE_REGISTRY_TOKEN}"
38 | # docker push docker.pkg.github.com/marley-nodejs/mern-stack-front-to-back-v2.0/api.anketa.info:latest
39 | # env:
40 | # GITHUB_PACKAGE_REGISTRY_TOKEN: ${{ secrets.GITHUB_PACKAGE_REGISTRY_TOKEN }}
41 |
--------------------------------------------------------------------------------
/.github/workflows/app2-sonar-cloud-scan.yml.disabled:
--------------------------------------------------------------------------------
1 | on:
2 | pull_request:
3 | types: [opened, synchronize, reopened]
4 | push:
5 | branches:
6 | - master
7 | paths:
8 | [ 'app2/app/client/**', 'app2/app/server/**' ]
9 | name: SonarCloud Trigger
10 | jobs:
11 | sonarcloud:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | with:
16 | # Disabling shallow clone is recommended for improving relevancy of reporting
17 | fetch-depth: 0
18 | - name: SonarCloud Scan
19 | uses: sonarsource/sonarcloud-github-action@master
20 | env:
21 | GITHUB_TOKEN: ${{ secrets.SONAR_GITHUB_TOKEN }}
22 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.~
2 | *.log
3 | node_modules/
4 | package-lock.json
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | # *.test.js
3 | # *.spec.js
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "bracketSpacing": true
4 | }
5 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # [NewLine] TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL [ENG, 2020]
2 |
3 |
4 |
5 | **Final Project**:
6 | https://www.tinyhouse.app/
7 |
8 |
9 |
10 | **Current Project Code Quality**:
11 | https://sonarcloud.io/dashboard?id=webmakaka_TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL
12 |
13 |
14 |
15 | ## How to run apps
16 |
17 | I am working in ubuntu 20.04.1 LTS
18 |
19 | Docker, Minikube, Kubectl, Skaffold should be installed.
20 |
21 |
22 |
23 | ### Docker
24 |
25 | ```
26 | $ docker -v
27 | Docker version 20.10.6, build 370c289
28 | ```
29 |
30 |
31 |
32 | ### Minikube installation
33 |
34 | ```
35 | $ curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/
36 |
37 | ```
38 |
39 |
40 |
41 | ```
42 | $ minikube version
43 | minikube version: v1.20.0
44 | ```
45 |
46 |
47 |
48 | ### Kubectl installation
49 |
50 | ```
51 | $ curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/
52 |
53 | $ kubectl version --client --short
54 | Client Version: v1.21.1
55 |
56 | ```
57 |
58 |
59 |
60 | ### Skaffold installation
61 |
62 | ```
63 | $ curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64
64 |
65 | $ chmod +x skaffold
66 | $ sudo mv skaffold /usr/local/bin
67 |
68 | $ skaffold version
69 | v1.25.0
70 | ```
71 |
72 |
73 |
74 | ### Run minikube
75 |
76 |
77 |
78 | ```
79 | $ {
80 | minikube --profile TinyHouse config set memory 8192
81 | minikube --profile TinyHouse config set cpus 4
82 |
83 | // minikube --profile TinyHouse config set vm-driver virtualbox
84 | minikube --profile TinyHouse config set vm-driver docker
85 |
86 | minikube --profile TinyHouse config set kubernetes-version v1.21.1
87 | minikube start --profile TinyHouse --embed-certs
88 | }
89 | ```
90 |
91 |
92 |
93 | ```
94 | // If needed start / stop / delete
95 | // $ minikube --profile TinyHouse start
96 | // $ minikube --profile TinyHouse stop
97 | // $ minikube --profile TinyHouse delete
98 | ```
99 |
100 |
101 |
102 | // Enable ingress
103 | $ minikube addons --profile TinyHouse enable ingress
104 |
105 |
106 |
107 | $ minikube --profile TinyHouse ip
108 | 192.168.49.2
109 |
110 |
111 |
112 | $ sudo vi /etc/hosts
113 |
114 | ```
115 | #---------------------------------------------------------------------
116 | # Minikube
117 | #---------------------------------------------------------------------
118 | 192.168.49.2 tinyhouse.dev
119 | ```
120 |
121 |
122 |
123 | ### k9s Installation (Optional)
124 |
125 |
126 |
127 | // homebrew install
128 | $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
129 |
130 | // commands from output of previous command
131 | // set your home, not mine
132 | $ echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /home/marley/.profile
133 | $ eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
134 |
135 | $ brew install k9s
136 |
137 |
138 |
139 | $ sudo vi /etc/profile.d/k9s.sh
140 |
141 |
142 |
143 | ```
144 | # set your home, not mine
145 | #### k9s #######################
146 |
147 | export MINIKUBE_HOME=/home/marley/.minikube
148 |
149 | #### k9s #######################
150 | ```
151 |
152 |
153 |
154 | ```
155 | $ sudo chmod 755 /etc/profile.d/k9s.sh
156 | $ source /etc/profile.d/k9s.sh
157 | ```
158 |
159 |
160 |
161 | $ k9s
162 |
163 |
164 |
165 | 
166 |
167 |
168 |
169 | 
170 |
171 |
172 |
173 | ### [App1](./app1/Readme.md)
174 |
175 | ### [App2](./app2/Readme.md)
176 |
177 | ---
178 |
179 |
180 |
181 | **Marley**
182 |
183 | Any questions in english: Telegram Chat
184 | Любые вопросы на русском: Телеграм чат
185 |
--------------------------------------------------------------------------------
/app1/Readme-postgresql.md:
--------------------------------------------------------------------------------
1 | # [NewLine] TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL [ENG, 2020]
2 |
3 |
4 |
5 | ### Run app from part 1
6 |
7 |
8 |
9 | $ cd app1
10 | $ cd postgresql
11 |
12 | $ npm install typeorm reflect-metadata
13 |
14 | $ npm install pg
15 |
16 |
17 |
18 | https://github.com/typeorm/typeorm
19 |
20 |
21 |
22 | $ npm run seed
23 | $ npm run start
24 |
25 |
26 |
27 | http://localhost:3000/api
28 |
29 | ```
30 | query{
31 | listings{
32 | id,
33 | title,
34 | image,
35 | address,
36 | price,
37 | numOfGuests,
38 | numOfBeds,
39 | numOfBaths,
40 | rating
41 | }
42 | }
43 | ```
44 |
45 | OK!
46 |
47 | ```
48 | query($id: ID!){
49 | listing(id: $id) {
50 | id,
51 | title,
52 | image,
53 | address,
54 | price,
55 | numOfGuests,
56 | numOfBeds,
57 | numOfBaths,
58 | rating
59 | }
60 | }
61 | ```
62 |
63 |
64 |
65 | ```
66 | {
67 | "id": "608cab1c6229455e8df0896ec344d386"
68 | }
69 | ```
70 |
71 | OK
72 |
73 | ```
74 | mutation{
75 | createListing{
76 | id,
77 | title,
78 | image,
79 | address,
80 | price,
81 | numOfGuests,
82 | numOfBeds,
83 | numOfBaths,
84 | rating
85 | }
86 | }
87 | ```
88 |
89 | OK
90 |
91 |
92 |
93 | ---
94 |
95 |
96 |
97 | **Marley**
98 |
99 | Any questions in english: Telegram Chat
100 | Любые вопросы на русском: Телеграм чат
101 |
--------------------------------------------------------------------------------
/app1/Readme.md:
--------------------------------------------------------------------------------
1 | # [NewLine] TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL [ENG, 2020]
2 |
3 |
4 |
5 | ### Run app from part 1
6 |
7 |
8 |
9 | $ cd app1
10 | $ cd skaffold
11 |
12 | $ docker login
13 |
14 |
15 |
16 | Need to update my docker image name webmakaka/tinyhouse\*\*\* to your in scripts from skaffold and k8s folders.
17 |
18 |
19 |
20 | $ skaffold dev
21 |
22 |
23 |
24 | ```
25 | $ kubectl get pods
26 | NAME READY STATUS RESTARTS AGE
27 | tinyhouse-client-deployment-d65d5b866-km548 1/1 Running 0 92s
28 | tinyhouse-mongo-deployment-755b899c89-qd44c 1/1 Running 0 92s
29 | tinyhouse-server-deployment-5744884c7c-lf8kp 1/1 Running 0 92s
30 | ```
31 |
32 |
33 |
34 | ```
35 | $ export POD_NAME=$(kubectl get pods --namespace default -l "app=tinyhouse-server" -o jsonpath="{.items[0].metadata.name}")
36 |
37 | $ kubectl exec -it ${POD_NAME} -- sh
38 | ```
39 |
40 |
41 |
42 | ```
43 | # cd /app/
44 | # npm run seed
45 | ```
46 |
47 |
48 |
49 | **chrome browser**
50 |
51 | ```
52 | client ---> https://tinyhouse.dev/
53 | graphql --> https://tinyhouse.dev/api/
54 | ```
55 |
56 |
57 |
58 | type: **thisisunsafe** in the browser window with security warning.
59 |
60 |
61 |
62 | https://tinyhouse.dev/api/
63 |
64 | ```
65 | query{
66 | listings{
67 | id,
68 | title,
69 | image
70 | }
71 | }
72 | ```
73 |
74 | **returns:**
75 |
76 | ```
77 | {
78 | "data": {
79 | "listings": [
80 | {
81 | "id": "5ff15c0989949700387558a1",
82 | "title": "Clean and fully furnished apartment. 5 min away from CN Tower",
83 | "image": "https://res.cloudinary.com/tiny-house/image/upload/v1560641352/mock/Toronto/toronto-listing-1_exv0tf.jpg"
84 | },
85 | {
86 | "id": "5ff15c0989949700387558a2",
87 | "title": "Luxurious home with private pool",
88 | "image": "https://res.cloudinary.com/tiny-house/image/upload/v1560645376/mock/Los%20Angeles/los-angeles-listing-1_aikhx7.jpg"
89 | },
90 | {
91 | "id": "5ff15c0989949700387558a3",
92 | "title": "Single bedroom located in the heart of downtown San Fransisco",
93 | "image": "https://res.cloudinary.com/tiny-house/image/upload/v1560646219/mock/San%20Fransisco/san-fransisco-listing-1_qzntl4.jpg"
94 | }
95 | ]
96 | }
97 | }
98 | ```
99 |
100 |
101 |
102 | https://tinyhouse.dev/
103 |
104 |
105 |
106 | **Expected result:**
107 |
108 |
109 |
110 | 
111 |
112 |
113 |
114 | ### Delete minikube with project
115 |
116 |
117 |
118 | ```
119 | $ minikube --profile TinyHouse stop && minikube --profile TinyHouse delete
120 | ```
121 |
122 |
123 |
124 | ---
125 |
126 |
127 |
128 | **Marley**
129 |
130 | Any questions in english: Telegram Chat
131 | Любые вопросы на русском: Телеграм чат
132 |
--------------------------------------------------------------------------------
/app1/app/client/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.eslintignore
2 | **/.eslintrc
3 | **/.prettierignore
4 | **/.prettierrc
5 |
6 | **/node_modules/
7 | **/node_modules_linux/
8 | **/Dockerfile
9 | **/.dockerignore
10 | **/.gitignore
11 | **/.git
12 | **/README.md
13 | **/Readme.md
14 | **/LICENSE
15 | **/.vscode
16 |
17 | **/test/
18 | **/.next/
--------------------------------------------------------------------------------
/app1/app/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | #.env.local
17 | #.env.development.local
18 | #.env.test.local
19 | #.env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/app1/app/client/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine3.12
2 |
3 | WORKDIR /app
4 | COPY package.json ./
5 |
6 | # without next command react-scripts are finishing and container restarts
7 | # at least in container node:12.20.0-alpine3.9
8 | # Possible bug and will be fixed in the future
9 | ENV CI=true
10 |
11 | #RUN npm install --only=prod --silent
12 | RUN npm install --silent
13 | COPY ./ ./
14 | CMD ["npm", "run", "start"]
--------------------------------------------------------------------------------
/app1/app/client/Dockerfile.prod:
--------------------------------------------------------------------------------
1 | # FROM node:12.16.3-alpine3.9
2 |
3 | # WORKDIR /app
4 | # COPY package.json ./
5 | # #RUN npm install --only=prod --silent
6 | # RUN npm install --silent
7 | # COPY ./ ./
8 | # CMD ["npm", "run", "start"]
9 |
10 |
11 |
12 | FROM node:lts-alpine3.12 as builder
13 |
14 | RUN mkdir -p /project
15 | WORKDIR '/project'
16 |
17 | COPY ./package*.json ./
18 | RUN npm install
19 |
20 | COPY . ./
21 |
22 | RUN npm run build
23 |
24 | FROM nginx
25 |
26 | # RUN apt update && apt upgrade && \
27 | # apt install -y bash vim less curl iputils-ping dnsutils
28 |
29 | COPY nginx.conf /etc/nginx/conf.d/default.conf
30 | COPY --from=builder /project/build /usr/share/nginx/html
--------------------------------------------------------------------------------
/app1/app/client/nginx.conf:
--------------------------------------------------------------------------------
1 | map $http_accept_language $lang {
2 | default en;
3 | ~^ru ru;
4 | }
5 |
6 | server {
7 | listen 3000 default_server;
8 | # listen 80;
9 | # server_name anketa.info;
10 |
11 | # if ( $http_x_forwarded_proto = 'http' ) {
12 | # return 301 https://$host$request_uri;
13 | # }
14 |
15 | root /usr/share/nginx/html;
16 |
17 | # location /api/ {
18 | # proxy_pass http://api.anketa.info;
19 | # }
20 |
21 | location / {
22 | # index index.html;
23 | try_files $uri /index.html;
24 | }
25 |
26 | location ~* \.(jpg|jpeg|png|gif|ico)$ {
27 | expires 30d;
28 | }
29 |
30 | location ~* \.(css|js)$ {
31 | expires 1d;
32 | }
33 |
34 | error_page 404 /404.html;
35 | location = /404.html {
36 | root /usr/share/nginx/html;
37 | internal;
38 | }
39 |
40 | error_page 500 502 503 504 /50x.html;
41 | location = /50x.html {
42 | root /usr/share/nginx/html;
43 | }
44 | }
45 |
46 | ## Compression.
47 | gzip on;
48 | gzip_buffers 16 8k;
49 | gzip_comp_level 1;
50 | gzip_http_version 1.1;
51 | gzip_min_length 10;
52 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/x-icon application/vnd.ms-fontobject font/opentype application/x-font-ttf;
53 | gzip_vary on;
54 | gzip_proxied any; # Compression for all requests.
55 | gzip_disable msie6;
--------------------------------------------------------------------------------
/app1/app/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tinyhouse-client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@apollo/react-hooks": "^4.0.0",
7 | "@types/graphql": "^14.2.3",
8 | "@types/jest": "^26.0.23",
9 | "@types/node": "^15.6.1",
10 | "@types/react": "17.0.9",
11 | "@types/react-dom": "17.0.6",
12 | "antd": "^4.16.1",
13 | "apollo-boost": "^0.4.9",
14 | "graphql": "^15.5.0",
15 | "react": "^17.0.2",
16 | "react-dom": "^17.0.2",
17 | "react-scripts": "^4.0.3",
18 | "typescript": "4.3.2"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject",
25 | "codegen:schema": "npx apollo client:download-schema --endpoint=https://tinyhouse.dev/api/",
26 | "codegen:generate": "npx apollo client:codegen --localSchemaFile=schema.json --includes=src/**/*.tsx --target=typescript"
27 | },
28 | "eslintConfig": {
29 | "extends": "react-app"
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app1/app/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app1/app/client/public/favicon.ico
--------------------------------------------------------------------------------
/app1/app/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | TinyHouse
10 |
11 |
12 | You need to enable JavaScript to run this app.
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app1/app/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/app1/app/client/src/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ApolloClient,
3 | ApolloProvider,
4 | createHttpLink,
5 | InMemoryCache,
6 | } from '@apollo/client';
7 | import React from 'react';
8 | import { render } from 'react-dom';
9 | import { Listings } from './sections';
10 | import './styles/index.css';
11 |
12 | const httpLink = createHttpLink({
13 | uri: '/api',
14 | });
15 |
16 | const client = new ApolloClient({
17 | link: httpLink,
18 | cache: new InMemoryCache(),
19 | });
20 |
21 | render(
22 |
23 |
24 | ,
25 | document.getElementById('root')
26 | );
27 |
--------------------------------------------------------------------------------
/app1/app/client/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/app1/app/client/src/sections/Listings/Listings.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { gql } from "apollo-boost";
3 | import { useQuery, useMutation } from "@apollo/react-hooks";
4 | import { Alert, Avatar, Button, List, Spin } from "antd";
5 | import { Listings as ListingsData } from "./__generated__/Listings";
6 | import { ListingsSkeleton } from "./components";
7 | import {
8 | DeleteListing as DeleteListingData,
9 | DeleteListingVariables
10 | } from "./__generated__/DeleteListing";
11 | import "./styles/Listings.css";
12 |
13 | const LISTINGS = gql`
14 | query Listings {
15 | listings {
16 | id
17 | title
18 | image
19 | address
20 | price
21 | numOfGuests
22 | numOfBeds
23 | numOfBaths
24 | rating
25 | }
26 | }
27 | `;
28 |
29 | const DELETE_LISTING = gql`
30 | mutation DeleteListing($id: ID!) {
31 | deleteListing(id: $id) {
32 | id
33 | }
34 | }
35 | `;
36 |
37 | interface Props {
38 | title: string;
39 | }
40 |
41 | export const Listings = ({ title }: Props) => {
42 | const { data, loading, error, refetch } = useQuery(LISTINGS);
43 |
44 | const [
45 | deleteListing,
46 | { loading: deleteListingLoading, error: deleteListingError }
47 | ] = useMutation(DELETE_LISTING);
48 |
49 | const handleDeleteListing = async (id: string) => {
50 | await deleteListing({ variables: { id } });
51 | refetch();
52 | };
53 |
54 | const listings = data ? data.listings : null;
55 |
56 | const listingsList = listings ? (
57 | (
61 | handleDeleteListing(listing.id)}
66 | >
67 | Delete
68 |
69 | ]}
70 | >
71 | }
75 | />
76 |
77 | )}
78 | />
79 | ) : null;
80 |
81 | if (loading) {
82 | return (
83 |
84 |
85 |
86 | );
87 | }
88 |
89 | if (error) {
90 | return (
91 |
92 |
93 |
94 | );
95 | }
96 |
97 | const deleteListingErrorAlert = deleteListingError ? (
98 |
103 | ) : null;
104 |
105 | return (
106 |
107 | {deleteListingErrorAlert}
108 |
109 | {title}
110 | {listingsList}
111 |
112 |
113 | );
114 | };
115 |
--------------------------------------------------------------------------------
/app1/app/client/src/sections/Listings/__generated__/DeleteListing.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // This file was automatically generated and should not be edited.
4 |
5 | // ====================================================
6 | // GraphQL mutation operation: DeleteListing
7 | // ====================================================
8 |
9 | export interface DeleteListing_deleteListing {
10 | __typename: "Listing";
11 | id: string;
12 | }
13 |
14 | export interface DeleteListing {
15 | deleteListing: DeleteListing_deleteListing;
16 | }
17 |
18 | export interface DeleteListingVariables {
19 | id: string;
20 | }
21 |
--------------------------------------------------------------------------------
/app1/app/client/src/sections/Listings/__generated__/Listings.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // This file was automatically generated and should not be edited.
4 |
5 | // ====================================================
6 | // GraphQL query operation: Listings
7 | // ====================================================
8 |
9 | export interface Listings_listings {
10 | __typename: "Listing";
11 | id: string;
12 | title: string;
13 | image: string;
14 | address: string;
15 | price: number;
16 | numOfGuests: number;
17 | numOfBeds: number;
18 | numOfBaths: number;
19 | rating: number;
20 | }
21 |
22 | export interface Listings {
23 | listings: Listings_listings[];
24 | }
25 |
--------------------------------------------------------------------------------
/app1/app/client/src/sections/Listings/components/ListingsSkeleton/ListingsSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Alert, Divider, Skeleton } from "antd";
3 | import "./styles/ListingsSkeleton.css";
4 |
5 | interface Props {
6 | title: string;
7 | error?: boolean;
8 | }
9 |
10 | export const ListingsSkeleton = ({ title, error = false }: Props) => {
11 | const errorAlert = error ? (
12 |
17 | ) : null;
18 |
19 | return (
20 |
21 | {errorAlert}
22 |
{title}
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/app1/app/client/src/sections/Listings/components/ListingsSkeleton/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ListingsSkeleton";
2 |
--------------------------------------------------------------------------------
/app1/app/client/src/sections/Listings/components/ListingsSkeleton/styles/ListingsSkeleton.css:
--------------------------------------------------------------------------------
1 | .listings-skeleton
2 | .ant-skeleton-content
3 | .ant-skeleton-title
4 | + .ant-skeleton-paragraph {
5 | margin-top: 12px;
6 | }
7 |
8 | .listings-skeleton .ant-divider-horizontal {
9 | margin: 12px 0;
10 | }
11 |
12 | .listings-skeleton .listings-skeleton__alert {
13 | margin-bottom: 20px;
14 | }
15 |
--------------------------------------------------------------------------------
/app1/app/client/src/sections/Listings/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ListingsSkeleton";
2 |
--------------------------------------------------------------------------------
/app1/app/client/src/sections/Listings/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Listings";
2 |
--------------------------------------------------------------------------------
/app1/app/client/src/sections/Listings/styles/Listings.css:
--------------------------------------------------------------------------------
1 | .listings {
2 | margin: 20px;
3 | max-width: 750px;
4 | }
5 |
6 | .listings .listings__alert {
7 | margin-bottom: 20px;
8 | }
9 |
--------------------------------------------------------------------------------
/app1/app/client/src/sections/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Listings";
2 |
--------------------------------------------------------------------------------
/app1/app/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "target": "ES2020",
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext"
9 | ],
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "esModuleInterop": true,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx",
22 | "noFallthroughCasesInSwitch": true
23 | },
24 | "include": [
25 | "src"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/app1/app/server/mongodb/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.eslintignore
2 | **/.eslintrc
3 | **/.prettierignore
4 | **/.prettierrc
5 |
6 | **/node_modules/
7 | **/node_modules_linux/
8 | **/Dockerfile
9 | **/.dockerignore
10 | **/.gitignore
11 | **/.git
12 | **/README.md
13 | **/Readme.md
14 | **/LICENSE
15 | **/.vscode
16 |
17 | **/test/
18 | **/.next/
--------------------------------------------------------------------------------
/app1/app/server/mongodb/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "ecmaVersion": 2020,
5 | "sourceType": "module"
6 | },
7 | "extends": [
8 | "plugin:@typescript-eslint/recommended",
9 | "plugin:import/errors",
10 | "plugin:import/typescript"
11 | ],
12 | "env": { "node": true },
13 | "rules": {
14 | "indent": "off",
15 | "@typescript-eslint/indent": "off",
16 | "@typescript-eslint/explicit-function-return-type": "off",
17 | "import/no-relative-parent-imports": "error",
18 | "import/no-unresolved": "off",
19 | "import/no-default-export": "warn"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app1/app/server/mongodb/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules/
3 |
4 | # production
5 | build/
6 |
7 | # misc
8 | .DS_Store
9 |
10 | # environment variables
11 | #.env
12 |
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
--------------------------------------------------------------------------------
/app1/app/server/mongodb/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine3.12
2 |
3 | WORKDIR /app
4 | COPY package.json ./
5 | # RUN npm install --only=prod --silent
6 | RUN npm install --silent
7 | COPY ./ ./
8 | CMD ["npm", "run", "start"]
--------------------------------------------------------------------------------
/app1/app/server/mongodb/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tinyhouse-server",
3 | "version": "0.1.0",
4 | "dependencies": {
5 | "apollo-server-express": "^2.25.0",
6 | "body-parser": "^1.19.0",
7 | "express": "^4.17.1",
8 | "graphql": "^15.5.0",
9 | "lodash.merge": "^4.6.2",
10 | "mongodb": "^3.6.9"
11 | },
12 | "devDependencies": {
13 | "@types/body-parser": "^1.19.0",
14 | "@types/express": "^4.17.12",
15 | "@types/graphql": "^14.2.3",
16 | "@types/lodash.merge": "^4.6.6",
17 | "@types/mongodb": "^3.6.17",
18 | "@types/node": "^15.6.1",
19 | "@typescript-eslint/eslint-plugin": "^4.26.0",
20 | "@typescript-eslint/parser": "^4.26.0",
21 | "dotenv": "^10.0.0",
22 | "eslint": "^7.27.0",
23 | "eslint-plugin-import": "^2.23.4",
24 | "nodemon": "^2.0.7",
25 | "ts-node": "^10.0.0",
26 | "typescript": "^4.3.2"
27 | },
28 | "scripts": {
29 | "start": "NODE_PATH=./src nodemon src/index.ts",
30 | "seed": "NODE_PATH=./src ts-node seed/seed.ts",
31 | "build": "NODE_PATH=./src tsc -p ./",
32 | "lint": "NODE_PATH=./src eslint '**/*.ts'"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app1/app/server/mongodb/seed/seed.ts:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | import { connectDatabase } from 'database';
4 | import { Listing } from 'lib/types';
5 | import { ObjectId } from 'mongodb';
6 |
7 | const seed = async () => {
8 | try {
9 | console.log('[seed] : running...');
10 |
11 | const db = await connectDatabase();
12 | const listings: Listing[] = [
13 | {
14 | _id: new ObjectId(),
15 | title: 'Clean and fully furnished apartment. 5 min away from CN Tower',
16 | image:
17 | 'https://res.cloudinary.com/tiny-house/image/upload/v1560641352/mock/Toronto/toronto-listing-1_exv0tf.jpg',
18 | address: '3210 Scotchmere Dr W, Toronto, ON, CA',
19 | price: 10000,
20 | numOfGuests: 2,
21 | numOfBeds: 1,
22 | numOfBaths: 2,
23 | rating: 5,
24 | },
25 | {
26 | _id: new ObjectId(),
27 | title: 'Luxurious home with private pool',
28 | image:
29 | 'https://res.cloudinary.com/tiny-house/image/upload/v1560645376/mock/Los%20Angeles/los-angeles-listing-1_aikhx7.jpg',
30 | address: '100 Hollywood Hills Dr, Los Angeles, California',
31 | price: 15000,
32 | numOfGuests: 2,
33 | numOfBeds: 1,
34 | numOfBaths: 1,
35 | rating: 4,
36 | },
37 | {
38 | _id: new ObjectId(),
39 | title: 'Single bedroom located in the heart of downtown San Fransisco',
40 | image:
41 | 'https://res.cloudinary.com/tiny-house/image/upload/v1560646219/mock/San%20Fransisco/san-fransisco-listing-1_qzntl4.jpg',
42 | address: '200 Sunnyside Rd, San Fransisco, California',
43 | price: 25000,
44 | numOfGuests: 3,
45 | numOfBeds: 2,
46 | numOfBaths: 2,
47 | rating: 3,
48 | },
49 | ];
50 |
51 | for (const listing of listings) {
52 | await db.listings.insertOne(listing);
53 | }
54 |
55 | console.log('[seed] : success');
56 | } catch {
57 | throw new Error('failed to seed database');
58 | } finally {
59 | process.exit();
60 | }
61 | };
62 |
63 | seed();
64 |
--------------------------------------------------------------------------------
/app1/app/server/mongodb/src/database/index.ts:
--------------------------------------------------------------------------------
1 | import { Database } from 'lib/types';
2 | import { MongoClient } from 'mongodb';
3 |
4 | // const url = `mongodb+srv://${process.env.DB_USER}:${
5 | // process.env.DB_USER_PASSWORD
6 | // }@${process.env.DB_CLUSTER}.mongodb.net`;
7 |
8 | const url = `${process.env.MONGO_URI}`;
9 |
10 | export const connectDatabase = async (): Promise => {
11 | const client = await MongoClient.connect(url, {
12 | useNewUrlParser: true,
13 | useUnifiedTopology: true,
14 | });
15 |
16 | const db = client.db('main');
17 |
18 | return {
19 | listings: db.collection('test_listings'),
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/app1/app/server/mongodb/src/graphql/index.ts:
--------------------------------------------------------------------------------
1 | export * from './resolvers';
2 | export * from './typeDefs';
3 |
--------------------------------------------------------------------------------
/app1/app/server/mongodb/src/graphql/resolvers/Listing/index.ts:
--------------------------------------------------------------------------------
1 | import { IResolvers } from 'apollo-server-express';
2 | import { Database, Listing } from 'lib/types';
3 | import { ObjectId } from 'mongodb';
4 |
5 | export const listingResolvers: IResolvers = {
6 | Query: {
7 | listings: async (
8 | _root: undefined,
9 | _args: {},
10 | { db }: { db: Database }
11 | ): Promise => {
12 | return await db.listings.find({}).toArray();
13 | },
14 | },
15 | Mutation: {
16 | deleteListing: async (
17 | _root: undefined,
18 | { id }: { id: string },
19 | { db }: { db: Database }
20 | ): Promise => {
21 | const deleteRes = await db.listings.findOneAndDelete({
22 | _id: new ObjectId(id),
23 | });
24 |
25 | if (!deleteRes.value) {
26 | throw new Error('failed to delete listing');
27 | }
28 |
29 | return deleteRes.value;
30 | },
31 | },
32 | Listing: {
33 | id: (listing: Listing): string => listing._id.toString(),
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/app1/app/server/mongodb/src/graphql/resolvers/index.ts:
--------------------------------------------------------------------------------
1 | import merge from "lodash.merge";
2 | import { listingResolvers } from "./Listing";
3 |
4 | export const resolvers = merge(listingResolvers);
5 |
--------------------------------------------------------------------------------
/app1/app/server/mongodb/src/graphql/typeDefs.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server-express';
2 |
3 | export const typeDefs = gql`
4 | type Listing {
5 | id: ID!
6 | title: String!
7 | image: String!
8 | address: String!
9 | price: Int!
10 | numOfGuests: Int!
11 | numOfBeds: Int!
12 | numOfBaths: Int!
13 | rating: Float!
14 | }
15 |
16 | type Query {
17 | listings: [Listing!]!
18 | }
19 |
20 | type Mutation {
21 | deleteListing(id: ID!): Listing!
22 | }
23 | `;
24 |
--------------------------------------------------------------------------------
/app1/app/server/mongodb/src/index.ts:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | import { ApolloServer } from 'apollo-server-express';
4 | import express, { Application } from 'express';
5 | import { connectDatabase } from './database';
6 | import { resolvers, typeDefs } from './graphql';
7 |
8 | const envChecks = async () => {
9 | if (!process.env.MONGO_URI) {
10 | throw new Error('Error: MONGO_URI must be defined');
11 | }
12 | };
13 |
14 | envChecks();
15 |
16 | const mount = async (app: Application) => {
17 | const db = await connectDatabase();
18 | const server = new ApolloServer({
19 | typeDefs,
20 | resolvers,
21 | context: () => ({ db }),
22 | });
23 |
24 | server.applyMiddleware({ app, path: '/api' });
25 |
26 | app.listen(process.env.GRAPHQL_PORT, () => {
27 | console.log(`[app] : http://localhost:${process.env.GRAPHQL_PORT}/api/`);
28 | });
29 | };
30 |
31 | mount(express());
32 |
--------------------------------------------------------------------------------
/app1/app/server/mongodb/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { Collection, ObjectId } from 'mongodb';
2 |
3 | export interface Listing {
4 | _id: ObjectId;
5 | title: string;
6 | image: string;
7 | address: string;
8 | price: number;
9 | numOfGuests: number;
10 | numOfBeds: number;
11 | numOfBaths: number;
12 | rating: number;
13 | }
14 |
15 | export interface Database {
16 | listings: Collection;
17 | }
18 |
--------------------------------------------------------------------------------
/app1/app/server/mongodb/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "target": "ES2020",
5 | "module": "commonjs",
6 | "rootDir": "./src",
7 | "outDir": "./build",
8 | "esModuleInterop": true,
9 | "strict": true
10 | },
11 | "exclude": ["temp"]
12 | }
13 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.eslintignore
2 | **/.eslintrc
3 | **/.prettierignore
4 | **/.prettierrc
5 |
6 | **/node_modules/
7 | **/node_modules_linux/
8 | **/Dockerfile
9 | **/.dockerignore
10 | **/.gitignore
11 | **/.git
12 | **/README.md
13 | **/Readme.md
14 | **/LICENSE
15 | **/.vscode
16 |
17 | **/test/
18 | **/.next/
--------------------------------------------------------------------------------
/app1/app/server/postgresql/.env:
--------------------------------------------------------------------------------
1 | GRAPHQL_PORT=3000
2 | DATABASE_HOST=localhost
3 | DATABASE_NAME=postgres
4 | DATABASE_PORT=5432
5 | DATABASE_USER=postgres
6 | DATABASE_PASSWORD=pass123
7 | SECRET_KEY=1HyftmH8tPzutU46s2MXM87QuF844WKm
--------------------------------------------------------------------------------
/app1/app/server/postgresql/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "ecmaVersion": 2020,
5 | "sourceType": "module"
6 | },
7 | "extends": [
8 | "plugin:@typescript-eslint/recommended",
9 | "plugin:import/errors",
10 | "plugin:import/typescript"
11 | ],
12 | "env": { "node": true },
13 | "rules": {
14 | "indent": "off",
15 | "@typescript-eslint/indent": "off",
16 | "@typescript-eslint/explicit-function-return-type": "off",
17 | "import/no-relative-parent-imports": "error",
18 | "import/no-unresolved": "off",
19 | "import/no-default-export": "warn"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules/
3 |
4 | # production
5 | build/
6 |
7 | # misc
8 | .DS_Store
9 |
10 | # environment variables
11 | #.env
12 |
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
--------------------------------------------------------------------------------
/app1/app/server/postgresql/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine3.12
2 |
3 | WORKDIR /app
4 | COPY package.json ./
5 | # RUN npm install --only=prod --silent
6 | RUN npm install --silent
7 | COPY ./ ./
8 | CMD ["npm", "run", "start"]
--------------------------------------------------------------------------------
/app1/app/server/postgresql/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | db:
4 | image: postgres
5 | restart: always
6 | ports:
7 | - '5432:5432'
8 | environment:
9 | POSTGRES_PASSWORD: pass123
10 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/ormconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "postgres",
3 | "host": "localhost",
4 | "port": 5432,
5 | "username": "postgres",
6 | "password": "pass123",
7 | "database": "postgres",
8 | "synchronize": true,
9 | "logging": false,
10 | "entities": ["src/database/entity/**/*.ts"],
11 | "migrations": ["src/database/migration/**/*.ts"],
12 | "subscribers": ["src/database/subscriber/**/*.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tinyhouse-server",
3 | "version": "0.1.0",
4 | "dependencies": {
5 | "apollo-server-express": "^2.25.0",
6 | "body-parser": "^1.19.0",
7 | "express": "^4.17.1",
8 | "graphql": "^15.5.0",
9 | "lodash.merge": "^4.6.2",
10 | "pg": "^8.6.0",
11 | "reflect-metadata": "^0.1.13",
12 | "typeorm": "^0.2.33"
13 | },
14 | "devDependencies": {
15 | "@types/body-parser": "^1.19.0",
16 | "@types/express": "^4.17.12",
17 | "@types/graphql": "^14.2.3",
18 | "@types/lodash.merge": "^4.6.6",
19 | "@types/node": "^15.6.1",
20 | "@typescript-eslint/eslint-plugin": "^4.26.0",
21 | "@typescript-eslint/parser": "^4.26.0",
22 | "dotenv": "^10.0.0",
23 | "eslint": "^7.27.0",
24 | "eslint-plugin-import": "^2.23.4",
25 | "nodemon": "^2.0.7",
26 | "ts-node": "^10.0.0",
27 | "typescript": "^4.3.2"
28 | },
29 | "scripts": {
30 | "start": "NODE_PATH=./src nodemon src/index.ts",
31 | "seed": "NODE_PATH=./src ts-node seed/seed.ts",
32 | "build": "NODE_PATH=./src tsc -p ./",
33 | "lint": "NODE_PATH=./src eslint '**/*.ts'"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/seed/seed.ts:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | import crypto from 'crypto';
4 | import { connectDatabase } from 'database';
5 | import { Listing } from 'lib/types';
6 |
7 | const seed = async () => {
8 | try {
9 | console.log('[seed] : running...');
10 |
11 | const db = await connectDatabase();
12 | const listings: Listing[] = [
13 | {
14 | id: crypto.randomBytes(16).toString('hex'),
15 | title: 'Clean and fully furnished apartment. 5 min away from CN Tower',
16 | image:
17 | 'https://res.cloudinary.com/tiny-house/image/upload/v1560641352/mock/Toronto/toronto-listing-1_exv0tf.jpg',
18 | address: '3210 Scotchmere Dr W, Toronto, ON, CA',
19 | price: 10000,
20 | numOfGuests: 2,
21 | numOfBeds: 1,
22 | numOfBaths: 2,
23 | rating: 5,
24 | },
25 | {
26 | id: crypto.randomBytes(16).toString('hex'),
27 | title: 'Luxurious home with private pool',
28 | image:
29 | 'https://res.cloudinary.com/tiny-house/image/upload/v1560645376/mock/Los%20Angeles/los-angeles-listing-1_aikhx7.jpg',
30 | address: '100 Hollywood Hills Dr, Los Angeles, California',
31 | price: 15000,
32 | numOfGuests: 2,
33 | numOfBeds: 1,
34 | numOfBaths: 1,
35 | rating: 4,
36 | },
37 | {
38 | id: crypto.randomBytes(16).toString('hex'),
39 | title: 'Single bedroom located in the heart of downtown San Fransisco',
40 | image:
41 | 'https://res.cloudinary.com/tiny-house/image/upload/v1560646219/mock/San%20Fransisco/san-fransisco-listing-1_qzntl4.jpg',
42 | address: '200 Sunnyside Rd, San Fransisco, California',
43 | price: 25000,
44 | numOfGuests: 3,
45 | numOfBeds: 2,
46 | numOfBaths: 2,
47 | rating: 3,
48 | },
49 | ];
50 |
51 | for (const listing of listings) {
52 | await db.listings.create(listing).save();
53 | }
54 |
55 | console.log('[seed] : success');
56 | } catch {
57 | throw new Error('failed to seed database');
58 | } finally {
59 | process.exit();
60 | }
61 | };
62 |
63 | seed();
64 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/src/database/entity/ListingEntity.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm';
2 |
3 | /* eslint-disable @typescript-eslint/explicit-member-accessibility */
4 |
5 | @Entity('test_listings')
6 | export class ListingEntity extends BaseEntity {
7 | @PrimaryColumn('text')
8 | id: string;
9 |
10 | @Column('varchar', { length: 100 })
11 | title: string;
12 |
13 | @Column('text')
14 | image: string;
15 |
16 | @Column('text')
17 | address: string;
18 |
19 | @Column('integer')
20 | price: number;
21 |
22 | @Column('integer')
23 | numOfGuests: number;
24 |
25 | @Column('integer')
26 | numOfBeds: number;
27 |
28 | @Column('integer')
29 | numOfBaths: number;
30 |
31 | @Column('decimal')
32 | rating: number;
33 | }
34 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/src/database/entity/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ListingEntity';
2 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/src/database/index.ts:
--------------------------------------------------------------------------------
1 | import { ListingEntity } from 'database/entity';
2 | import { Database } from 'lib/types';
3 | import { createConnection } from 'typeorm';
4 |
5 | export const connectDatabase = async (): Promise => {
6 | const connection = await createConnection();
7 |
8 | return {
9 | listings: connection.getRepository(ListingEntity),
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/src/graphql/index.ts:
--------------------------------------------------------------------------------
1 | export * from './resolvers';
2 | export * from './typeDefs';
3 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/src/graphql/resolvers/Listing/index.ts:
--------------------------------------------------------------------------------
1 | import { IResolvers } from 'apollo-server-express';
2 | import crypto from 'crypto';
3 | import { Database, Listing } from 'lib/types';
4 |
5 | export const listingResolvers: IResolvers = {
6 | Query: {
7 | listing: async (
8 | _root: undefined,
9 | { id }: { id: string },
10 | { db }: { db: Database }
11 | ): Promise => {
12 | const listing = await db.listings.findOne({ id });
13 |
14 | if (!listing) {
15 | throw new Error(`[App] Failed to find listing with id: ${id}`);
16 | }
17 |
18 | return listing;
19 | },
20 |
21 | listings: async (
22 | _root: undefined,
23 | _args: {},
24 | { db }: { db: Database }
25 | ): Promise => {
26 | return await db.listings.find({});
27 | },
28 | },
29 | Mutation: {
30 | createListing: async (
31 | _root: undefined,
32 | _args: {},
33 | { db }: { db: Database }
34 | ): Promise => {
35 | const newListing = {
36 | id: crypto.randomBytes(16).toString('hex'),
37 | title: 'Clean and fully furnished apartment. 5 min away from CN Tower',
38 | image:
39 | 'https://res.cloudinary.com/tiny-house/image/upload/v1560641352/mock/Toronto/toronto-listing-1_exv0tf.jpg',
40 | address: '3210 Scotchmere Dr W, Toronto, ON, CA',
41 | price: 10000,
42 | numOfGuests: 2,
43 | numOfBeds: 1,
44 | numOfBaths: 2,
45 | rating: 5,
46 | };
47 |
48 | return await db.listings.create(newListing).save();
49 | },
50 |
51 | updateListing: async (
52 | _root: undefined,
53 | { id }: { id: string },
54 | { db }: { db: Database }
55 | ): Promise => {
56 | const listing = await db.listings.findOne({ id });
57 |
58 | if (!listing) {
59 | throw new Error(`[App] Failed to find listing with id: ${id}`);
60 | }
61 |
62 | listing.title = '[UPDATED] This is my updated title!';
63 |
64 | return await listing.save();
65 | },
66 |
67 | deleteListing: async (
68 | _root: undefined,
69 | { id }: { id: string },
70 | { db }: { db: Database }
71 | ): Promise => {
72 | const listing = await db.listings.findOne({ id });
73 | if (!listing) {
74 | throw new Error(`[App] Failed to find listing with id: ${id}`);
75 | }
76 | return await listing.remove();
77 | },
78 | },
79 | };
80 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/src/graphql/resolvers/index.ts:
--------------------------------------------------------------------------------
1 | import merge from "lodash.merge";
2 | import { listingResolvers } from "./Listing";
3 |
4 | export const resolvers = merge(listingResolvers);
5 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/src/graphql/typeDefs.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server-express';
2 |
3 | export const typeDefs = gql`
4 | type Listing {
5 | id: ID!
6 | title: String!
7 | image: String!
8 | address: String!
9 | price: Int!
10 | numOfGuests: Int!
11 | numOfBeds: Int!
12 | numOfBaths: Int!
13 | rating: Float!
14 | }
15 |
16 | type Query {
17 | listing(id: ID!): Listing!
18 | listings: [Listing!]!
19 | }
20 |
21 | type Mutation {
22 | createListing: Listing!
23 | updateListing(id: ID!): Listing!
24 | deleteListing(id: ID!): Listing!
25 | }
26 | `;
27 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/src/index.ts:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | import { ApolloServer } from 'apollo-server-express';
4 | import express, { Application } from 'express';
5 | import 'reflect-metadata';
6 | import { connectDatabase } from './database';
7 | import { resolvers, typeDefs } from './graphql';
8 |
9 | const mount = async (app: Application) => {
10 | const db = await connectDatabase();
11 | const server = new ApolloServer({
12 | typeDefs,
13 | resolvers,
14 | context: () => ({ db }),
15 | });
16 |
17 | server.applyMiddleware({ app, path: '/api' });
18 |
19 | app.listen(process.env.GRAPHQL_PORT, () => {
20 | console.log(`[app] : http://localhost:${process.env.GRAPHQL_PORT}/api/`);
21 | });
22 | };
23 |
24 | mount(express());
25 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { ListingEntity } from 'database/entity';
2 | import { Repository } from 'typeorm';
3 |
4 | export interface Listing {
5 | id: string;
6 | title: string;
7 | image: string;
8 | address: string;
9 | price: number;
10 | numOfGuests: number;
11 | numOfBeds: number;
12 | numOfBaths: number;
13 | rating: number;
14 | }
15 |
16 | export interface Database {
17 | listings: Repository;
18 | }
19 |
--------------------------------------------------------------------------------
/app1/app/server/postgresql/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "ES2020",
5 | "baseUrl": "./src",
6 | "rootDir": "./src",
7 | "outDir": "./build",
8 | "esModuleInterop": true,
9 | "strict": true,
10 | "emitDecoratorMetadata": true,
11 | "experimentalDecorators": true,
12 | "strictPropertyInitialization": false
13 | },
14 | "exclude": ["temp"]
15 | }
16 |
--------------------------------------------------------------------------------
/app1/k8s/client-clusterIP.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: tinyhouse-client-svc
5 | spec:
6 | selector:
7 | app: tinyhouse-client
8 | ports:
9 | - name: tinyhouse-client
10 | protocol: TCP
11 | port: 3000
12 | targetPort: 3000
13 |
--------------------------------------------------------------------------------
/app1/k8s/client-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: tinyhouse-client-deployment
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: tinyhouse-client
10 | template:
11 | metadata:
12 | labels:
13 | app: tinyhouse-client
14 | spec:
15 | containers:
16 | - name: tinyhouse-client
17 | image: webmakaka/tinyhouse-client-app1
18 |
--------------------------------------------------------------------------------
/app1/k8s/ingress-config.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: networking.k8s.io/v1
2 | kind: Ingress
3 | metadata:
4 | name: ingress-svc
5 | annotations:
6 | kubernetes.io/ingress.class: nginx
7 | nginx.ingress.kubernetes.io/use-regex: 'true'
8 | spec:
9 | rules:
10 | - host: tinyhouse.dev
11 | http:
12 | paths:
13 | - path: /api/?(.*)
14 | pathType: 'Prefix'
15 | backend:
16 | service:
17 | name: tinyhouse-server-svc
18 | port:
19 | number: 3000
20 | - path: /?(.*)
21 | pathType: 'Prefix'
22 | backend:
23 | service:
24 | name: tinyhouse-client-svc
25 | port:
26 | number: 3000
27 |
--------------------------------------------------------------------------------
/app1/k8s/mongo-clusterIP.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: tinyhouse-mongo-svc
5 | spec:
6 | selector:
7 | app: tinyhouse-mongo
8 | ports:
9 | - name: db
10 | protocol: TCP
11 | port: 27017
12 | targetPort: 27017
13 |
--------------------------------------------------------------------------------
/app1/k8s/mongo-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: tinyhouse-mongo-deployment
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: tinyhouse-mongo
10 | template:
11 | metadata:
12 | labels:
13 | app: tinyhouse-mongo
14 | spec:
15 | containers:
16 | - name: tinyhouse-mongo
17 | image: mongo:4.4.6-bionic
18 |
--------------------------------------------------------------------------------
/app1/k8s/server-clusterIP.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: tinyhouse-server-svc
5 | spec:
6 | selector:
7 | app: tinyhouse-server
8 | ports:
9 | - name: tinyhouse-server
10 | protocol: TCP
11 | port: 3000
12 | targetPort: 3000
13 |
--------------------------------------------------------------------------------
/app1/k8s/server-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: tinyhouse-server-deployment
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: tinyhouse-server
10 | template:
11 | metadata:
12 | labels:
13 | app: tinyhouse-server
14 | spec:
15 | containers:
16 | - name: tinyhouse-server
17 | image: webmakaka/tinyhouse-server-app1
18 | env:
19 | - name: MONGO_URI
20 | value: 'mongodb://tinyhouse-mongo-svc:27017'
21 | - name: GRAPHQL_PORT
22 | value: '3000'
23 |
--------------------------------------------------------------------------------
/app1/skaffold/skaffold.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: skaffold/v2beta17
2 | kind: Config
3 | build:
4 | local:
5 | push: false
6 | tagPolicy:
7 | sha256: {}
8 | artifacts:
9 | - image: webmakaka/tinyhouse-client-app1
10 | context: ../app/client
11 | docker:
12 | dockerfile: Dockerfile.dev
13 | sync:
14 | manual:
15 | - src: 'src/**/*.ts'
16 | dest: .
17 | - image: webmakaka/tinyhouse-server-app1
18 | context: ../app/server/mongodb
19 | docker:
20 | dockerfile: Dockerfile
21 | sync:
22 | manual:
23 | - src: 'src/**/*.ts'
24 | dest: .
25 | deploy:
26 | kubectl:
27 | manifests:
28 | - ../k8s/*
29 |
--------------------------------------------------------------------------------
/app2/Readme-postgresql.md:
--------------------------------------------------------------------------------
1 | # [NewLine] TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL [ENG, 2020]
2 |
3 |
4 |
5 | ### Run app from part 2
6 |
7 |
8 |
9 | $ cd app1
10 | $ cd postgresql
11 |
12 | $ npm install typeorm reflect-metadata
13 |
14 | $ npm install pg
15 |
16 |
17 |
18 | https://github.com/typeorm/typeorm
19 |
20 |
21 |
22 | ```
23 | $ npm run seed
24 | $ npm run start
25 | ```
26 |
27 |
28 |
29 | ```
30 | CREATE INDEX location_index ON public.listings (country, admin, city);
31 | ```
32 |
33 |
34 |
35 | http://localhost:3000/api
36 |
37 |
38 |
39 | Frontend. Possible need to update uri to:
40 |
41 |
42 |
43 | ```
44 | const httpLink = createHttpLink({
45 | uri: 'http://localhost:3000/api',
46 | });
47 | ```
48 |
49 |
50 |
51 | ---
52 |
53 |
54 |
55 | **Marley**
56 |
57 | Any questions in english: Telegram Chat
58 | Любые вопросы на русском: Телеграм чат
59 |
--------------------------------------------------------------------------------
/app2/app/client/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.eslintignore
2 | **/.eslintrc
3 | **/.prettierignore
4 | **/.prettierrc
5 |
6 | **/node_modules/
7 | **/node_modules_linux/
8 | **/Dockerfile
9 | **/.dockerignore
10 | **/.gitignore
11 | **/.git
12 | **/README.md
13 | **/Readme.md
14 | **/LICENSE
15 | **/.vscode
16 |
17 | **/test/
18 | **/.next/
--------------------------------------------------------------------------------
/app2/app/client/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_STRIPE_PUBLISHABLE_KEY=FAKE_REACT_APP_STRIPE_PUBLISHABLE_KEY
2 | REACT_APP_STRIPE_CONNECT_CLIENT_ID=FAKE_REACT_APP_STRIPE_CONNECT_CLIENT_ID
--------------------------------------------------------------------------------
/app2/app/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | #.env.local
17 | #.env.development.local
18 | #.env.test.local
19 | #.env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/app2/app/client/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine3.12
2 |
3 | WORKDIR /app
4 | COPY package.json ./
5 |
6 | # without next command react-scripts are finishing and container restarts
7 | # at least in container node:12.20.0-alpine3.9
8 | # Possible bug and will be fixed in the future
9 | ENV CI=true
10 |
11 | #RUN npm install --only=prod --silent
12 | RUN npm install --silent
13 | COPY ./ ./
14 | CMD ["npm", "run", "start"]
--------------------------------------------------------------------------------
/app2/app/client/Dockerfile.prod:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine3.12 as builder
2 |
3 | RUN mkdir -p /project
4 | WORKDIR '/project'
5 |
6 | COPY ./package*.json ./
7 | RUN npm install
8 |
9 | COPY . ./
10 |
11 | RUN npm run build
12 |
13 | FROM nginx
14 |
15 | RUN apt update && apt upgrade && \
16 | apt install -y bash vim less curl iputils-ping dnsutils
17 |
18 | COPY nginx.conf /etc/nginx/conf.d/default.conf
19 | COPY --from=builder /project/build /usr/share/nginx/html
--------------------------------------------------------------------------------
/app2/app/client/apollo.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | client: {
3 | service: {
4 | name: 'apollo-server',
5 | url: 'https://tinyhouse.dev/api',
6 | // optional headers
7 | // headers: {
8 | // authorization: 'Bearer lkjfalkfjadkfjeopknavadf',
9 | // },
10 | // optional disable SSL validation check
11 | skipSSLValidation: true,
12 | },
13 | },
14 | };
15 |
16 | // module.exports = {
17 | // service: {
18 | // endpoint: {
19 | // url: 'https://tinyhouse.dev/api',
20 | // skipSSLValidation: true,
21 | // },
22 | // },
23 | // };
24 |
25 | // module.exports = {
26 | // client: {
27 | // service: {
28 | // name: 'apollo-server',
29 | // endpoint: {
30 | // url: 'https://tinyhouse.dev/api',
31 | // skipSSLValidation: true,
32 | // },
33 | // },
34 | // },
35 | // };
36 |
--------------------------------------------------------------------------------
/app2/app/client/nginx.conf:
--------------------------------------------------------------------------------
1 | map $http_accept_language $lang {
2 | default en;
3 | }
4 |
5 | server {
6 | listen 3000 default_server;
7 | root /usr/share/nginx/html;
8 |
9 | location / {
10 | try_files $uri /index.html;
11 | }
12 |
13 | location ~* \.(jpg|jpeg|png|gif|ico)$ {
14 | expires 30d;
15 | }
16 |
17 | location ~* \.(css|js)$ {
18 | expires 1d;
19 | }
20 |
21 | error_page 404 /404.html;
22 | location = /404.html {
23 | root /usr/share/nginx/html;
24 | internal;
25 | }
26 |
27 | error_page 500 502 503 504 /50x.html;
28 | location = /50x.html {
29 | root /usr/share/nginx/html;
30 | }
31 | }
32 |
33 | ## Compression.
34 | gzip on;
35 | gzip_buffers 16 8k;
36 | gzip_comp_level 1;
37 | gzip_http_version 1.1;
38 | gzip_min_length 10;
39 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/x-icon application/vnd.ms-fontobject font/opentype application/x-font-ttf;
40 | gzip_vary on;
41 | gzip_proxied any; # Compression for all requests.
42 | gzip_disable msie6;
--------------------------------------------------------------------------------
/app2/app/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tinyhouse-client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@ant-design/icons": "^4.6.2",
7 | "@apollo/client": "^3.3.20",
8 | "@apollo/react-hooks": "^4.0.0",
9 | "@types/graphql": "^14.5.0",
10 | "@types/jest": "^26.0.23",
11 | "@types/node": "^15.12.2",
12 | "@types/react": "^17.0.10",
13 | "@types/react-dom": "^17.0.7",
14 | "@types/react-router-dom": "^5.1.7",
15 | "@types/react-stripe-elements": "^6.0.4",
16 | "antd": "^4.16.2",
17 | "apollo-boost": "^0.4.9",
18 | "graphql": "^15.5.0",
19 | "moment": "^2.29.1",
20 | "react": "^17.0.2",
21 | "react-dom": "^17.0.2",
22 | "react-router-dom": "^5.2.0",
23 | "react-scripts": "^4.0.3",
24 | "react-stripe-elements": "^6.1.2",
25 | "typescript": "^4.3.2"
26 | },
27 | "scripts": {
28 | "start": "PORT=3000 HOST=0.0.0.0 react-scripts start",
29 | "build": "react-scripts build",
30 | "test": "react-scripts test",
31 | "eject": "react-scripts eject",
32 | "codegen:schema": "npx apollo client:download-schema schema.json",
33 | "codegen:generate": "npx apollo client:codegen --localSchemaFile=schema.json --includes=src/**/*.ts --globalTypesFile=./src/lib/graphql/globalTypes.ts --target=typescript"
34 | },
35 | "eslintConfig": {
36 | "extends": "react-app"
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "last 1 chrome version",
46 | "last 1 firefox version",
47 | "last 1 safari version"
48 | ]
49 | },
50 | "volta": {
51 | "node": "14.15.3",
52 | "npm": "6.14.10"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app2/app/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app2/app/client/public/favicon.ico
--------------------------------------------------------------------------------
/app2/app/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | TinyHouse
10 |
11 |
12 |
13 | You need to enable JavaScript to run this app.
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app2/app/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/components/AppHeaderSkeleton/assets/tinyhouse-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app2/app/client/src/lib/components/AppHeaderSkeleton/assets/tinyhouse-logo.png
--------------------------------------------------------------------------------
/app2/app/client/src/lib/components/AppHeaderSkeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from 'antd';
2 | import logo from './assets/tinyhouse-logo.png';
3 |
4 | const { Header } = Layout;
5 |
6 | export const AppHeaderSkeleton = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/components/ErrorBanner/index.tsx:
--------------------------------------------------------------------------------
1 | import { Alert } from 'antd';
2 |
3 | interface IProps {
4 | message?: string;
5 | description?: string;
6 | }
7 |
8 | export const ErrorBanner = ({
9 | message = 'Uh oh! Something went wrong!',
10 | description = 'Look like somethig went wrong! Please check your connection and/or try again later!',
11 | }: IProps) => {
12 | return (
13 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/components/ListingCard/index.tsx:
--------------------------------------------------------------------------------
1 | import { UserOutlined } from '@ant-design/icons';
2 | import { Card, Typography } from 'antd';
3 | import { Link } from 'react-router-dom';
4 | import { formatListingPrice, iconColor } from '../../utils';
5 |
6 | interface IProps {
7 | listing: {
8 | id: string;
9 | title: string;
10 | image: string;
11 | address: string;
12 | price: number;
13 | numOfGuests: number;
14 | };
15 | }
16 |
17 | const { Text, Title } = Typography;
18 |
19 | export const ListingCard = ({ listing }: IProps) => {
20 | const { id, title, image, address, price, numOfGuests } = listing;
21 | return (
22 |
23 |
30 | }
31 | >
32 |
33 |
34 |
35 | {formatListingPrice(price)}
36 | /day
37 |
38 |
39 | {title}
40 |
41 |
42 | {address}
43 |
44 |
45 |
46 | {' '}
47 | {numOfGuests} guests
48 |
49 |
50 |
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/components/PageSkeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from 'antd';
2 | import { Fragment } from 'react';
3 |
4 | export const PageSkeleton = () => {
5 | const skeletonParagraph = (
6 |
11 | );
12 |
13 | return (
14 |
15 | {skeletonParagraph}
16 | {skeletonParagraph}
17 | {skeletonParagraph}
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './AppHeaderSkeleton';
2 | export * from './ErrorBanner';
3 | export * from './PageSkeleton';
4 | export * from './ListingCard';
5 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/globalTypes.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 | //==============================================================
7 | // START Enums and Input Objects
8 | //==============================================================
9 |
10 | export enum ListingType {
11 | APARTMENT = "APARTMENT",
12 | HOUSE = "HOUSE",
13 | }
14 |
15 | export enum ListingsFilter {
16 | PRICE_HIGH_TO_LOW = "PRICE_HIGH_TO_LOW",
17 | PRICE_LOW_TO_HIGH = "PRICE_LOW_TO_HIGH",
18 | }
19 |
20 | export interface ConnectStripeInput {
21 | code: string;
22 | }
23 |
24 | export interface CreateBookingInput {
25 | id: string;
26 | source: string;
27 | checkIn: string;
28 | checkOut: string;
29 | }
30 |
31 | export interface HostListingInput {
32 | title: string;
33 | description: string;
34 | image: string;
35 | type: ListingType;
36 | address: string;
37 | price: number;
38 | numOfGuests: number;
39 | }
40 |
41 | export interface LogInInput {
42 | code: string;
43 | }
44 |
45 | //==============================================================
46 | // END Enums and Input Objects
47 | //==============================================================
48 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/mutations/ConnectStripe/__generated__/ConnectStripe.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 | import { ConnectStripeInput } from "./../../../globalTypes";
7 |
8 | // ====================================================
9 | // GraphQL mutation operation: ConnectStripe
10 | // ====================================================
11 |
12 | export interface ConnectStripe_connectStripe {
13 | __typename: "Viewer";
14 | hasWallet: boolean | null;
15 | }
16 |
17 | export interface ConnectStripe {
18 | connectStripe: ConnectStripe_connectStripe;
19 | }
20 |
21 | export interface ConnectStripeVariables {
22 | input: ConnectStripeInput;
23 | }
24 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/mutations/ConnectStripe/index.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-boost';
2 |
3 | export const CONNECT_STRIPE = gql`
4 | mutation ConnectStripe($input: ConnectStripeInput!) {
5 | connectStripe(input: $input) {
6 | hasWallet
7 | }
8 | }
9 | `;
10 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/mutations/CreateBooking/__generated__/CreateBooking.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 | import { CreateBookingInput } from "./../../../globalTypes";
7 |
8 | // ====================================================
9 | // GraphQL mutation operation: CreateBooking
10 | // ====================================================
11 |
12 | export interface CreateBooking_createBooking {
13 | __typename: "Booking";
14 | id: string;
15 | }
16 |
17 | export interface CreateBooking {
18 | createBooking: CreateBooking_createBooking;
19 | }
20 |
21 | export interface CreateBookingVariables {
22 | input: CreateBookingInput;
23 | }
24 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/mutations/CreateBooking/index.ts:
--------------------------------------------------------------------------------
1 | import {gql} from 'apollo-boost';
2 |
3 | export const CREATE_BOOKING = gql`
4 | mutation CreateBooking($input: CreateBookingInput!) {
5 | createBooking(input: $input) {
6 | id
7 | }
8 | }
9 | `;
10 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/mutations/DisconnectStripe/__generated__/DisconnectStripe.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 | // ====================================================
7 | // GraphQL mutation operation: DisconnectStripe
8 | // ====================================================
9 |
10 | export interface DisconnectStripe_disconnectStripe {
11 | __typename: "Viewer";
12 | hasWallet: boolean | null;
13 | }
14 |
15 | export interface DisconnectStripe {
16 | disconnectStripe: DisconnectStripe_disconnectStripe;
17 | }
18 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/mutations/DisconnectStripe/index.ts:
--------------------------------------------------------------------------------
1 | import {gql} from 'apollo-boost';
2 |
3 | export const DISCONNECT_STRIPE = gql`
4 | mutation DisconnectStripe {
5 | disconnectStripe {
6 | hasWallet
7 | }
8 | }
9 | `;
10 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/mutations/HostListing/__generated__/HostListing.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 | import { HostListingInput } from "./../../../globalTypes";
7 |
8 | // ====================================================
9 | // GraphQL mutation operation: HostListing
10 | // ====================================================
11 |
12 | export interface HostListing_hostListing {
13 | __typename: "Listing";
14 | id: string;
15 | }
16 |
17 | export interface HostListing {
18 | hostListing: HostListing_hostListing;
19 | }
20 |
21 | export interface HostListingVariables {
22 | input: HostListingInput;
23 | }
24 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/mutations/HostListing/index.ts:
--------------------------------------------------------------------------------
1 | import {gql} from 'apollo-boost';
2 |
3 | export const HOST_LISTING = gql`
4 | mutation HostListing($input: HostListingInput!) {
5 | hostListing(input: $input) {
6 | id
7 | }
8 | }
9 | `;
10 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/mutations/LogIn/__generated__/LogIn.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 | import { LogInInput } from "./../../../globalTypes";
7 |
8 | // ====================================================
9 | // GraphQL mutation operation: LogIn
10 | // ====================================================
11 |
12 | export interface LogIn_logIn {
13 | __typename: "Viewer";
14 | id: string | null;
15 | token: string | null;
16 | avatar: string | null;
17 | hasWallet: boolean | null;
18 | didRequest: boolean;
19 | }
20 |
21 | export interface LogIn {
22 | logIn: LogIn_logIn;
23 | }
24 |
25 | export interface LogInVariables {
26 | input?: LogInInput | null;
27 | }
28 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/mutations/LogIn/index.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-boost';
2 |
3 | export const LOG_IN = gql`
4 | mutation LogIn($input: LogInInput) {
5 | logIn(input: $input) {
6 | id
7 | token
8 | avatar
9 | hasWallet
10 | didRequest
11 | }
12 | }
13 | `;
14 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/mutations/LogOut/__generated__/LogOut.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 | // ====================================================
7 | // GraphQL mutation operation: LogOut
8 | // ====================================================
9 |
10 | export interface LogOut_logOut {
11 | __typename: "Viewer";
12 | id: string | null;
13 | token: string | null;
14 | avatar: string | null;
15 | hasWallet: boolean | null;
16 | didRequest: boolean;
17 | }
18 |
19 | export interface LogOut {
20 | logOut: LogOut_logOut;
21 | }
22 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/mutations/LogOut/index.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-boost';
2 |
3 | export const LOG_OUT = gql`
4 | mutation LogOut {
5 | logOut {
6 | id
7 | token
8 | avatar
9 | hasWallet
10 | didRequest
11 | }
12 | }
13 | `;
14 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/mutations/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ConnectStripe';
2 | export * from './CreateBooking';
3 | export * from './DisconnectStripe';
4 | export * from './HostListing';
5 | export * from './LogIn';
6 | export * from './LogOut';
7 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/queries/AuthUrl/__generated__/AuthUrl.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 | // ====================================================
7 | // GraphQL query operation: AuthUrl
8 | // ====================================================
9 |
10 | export interface AuthUrl {
11 | authUrl: string;
12 | }
13 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/queries/AuthUrl/index.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-boost';
2 |
3 | export const AUTH_URL = gql`
4 | query AuthUrl {
5 | authUrl
6 | }
7 | `;
8 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/queries/Listing/__generated__/Listing.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 | import { ListingType } from "./../../../globalTypes";
7 |
8 | // ====================================================
9 | // GraphQL query operation: Listing
10 | // ====================================================
11 |
12 | export interface Listing_listing_host {
13 | __typename: "User";
14 | id: string;
15 | name: string;
16 | avatar: string;
17 | hasWallet: boolean;
18 | }
19 |
20 | export interface Listing_listing_bookings_result_tenant {
21 | __typename: "User";
22 | id: string;
23 | name: string;
24 | avatar: string;
25 | }
26 |
27 | export interface Listing_listing_bookings_result {
28 | __typename: "Booking";
29 | id: string;
30 | tenant: Listing_listing_bookings_result_tenant;
31 | checkIn: string;
32 | checkOut: string;
33 | }
34 |
35 | export interface Listing_listing_bookings {
36 | __typename: "Bookings";
37 | total: number;
38 | result: Listing_listing_bookings_result[];
39 | }
40 |
41 | export interface Listing_listing {
42 | __typename: "Listing";
43 | id: string;
44 | title: string;
45 | description: string;
46 | image: string;
47 | host: Listing_listing_host;
48 | type: ListingType;
49 | address: string;
50 | city: string;
51 | bookings: Listing_listing_bookings | null;
52 | bookingsIndex: string;
53 | price: number;
54 | numOfGuests: number;
55 | }
56 |
57 | export interface Listing {
58 | listing: Listing_listing;
59 | }
60 |
61 | export interface ListingVariables {
62 | id: string;
63 | bookingsPage: number;
64 | limit: number;
65 | }
66 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/queries/Listing/index.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-boost';
2 |
3 | export const LISTING = gql`
4 | query Listing($id: ID!, $bookingsPage: Int!, $limit: Int!) {
5 | listing(id: $id) {
6 | id
7 | title
8 | description
9 | image
10 | host {
11 | id
12 | name
13 | avatar
14 | hasWallet
15 | }
16 | type
17 | address
18 | city
19 | bookings(limit: $limit, page: $bookingsPage) {
20 | total
21 | result {
22 | id
23 | tenant {
24 | id
25 | name
26 | avatar
27 | }
28 | checkIn
29 | checkOut
30 | }
31 | }
32 | bookingsIndex
33 | price
34 | numOfGuests
35 | }
36 | }
37 | `;
38 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/queries/Listings/__generated__/Listings.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 | import { ListingsFilter } from "./../../../globalTypes";
7 |
8 | // ====================================================
9 | // GraphQL query operation: Listings
10 | // ====================================================
11 |
12 | export interface Listings_listings_result {
13 | __typename: "Listing";
14 | id: string;
15 | title: string;
16 | image: string;
17 | address: string;
18 | price: number;
19 | numOfGuests: number;
20 | }
21 |
22 | export interface Listings_listings {
23 | __typename: "Listings";
24 | region: string | null;
25 | total: number;
26 | result: Listings_listings_result[];
27 | }
28 |
29 | export interface Listings {
30 | listings: Listings_listings;
31 | }
32 |
33 | export interface ListingsVariables {
34 | location?: string | null;
35 | filter: ListingsFilter;
36 | limit: number;
37 | page: number;
38 | }
39 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/queries/Listings/index.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-boost';
2 |
3 | export const LISTINGS = gql`
4 | query Listings(
5 | $location: String
6 | $filter: ListingsFilter!
7 | $limit: Int!
8 | $page: Int!
9 | ) {
10 | listings(location: $location, filter: $filter, limit: $limit, page: $page) {
11 | region
12 | total
13 | result {
14 | id
15 | title
16 | image
17 | address
18 | price
19 | numOfGuests
20 | }
21 | }
22 | }
23 | `;
24 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/queries/User/__generated__/User.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 | /* eslint-disable */
3 | // @generated
4 | // This file was automatically generated and should not be edited.
5 |
6 | // ====================================================
7 | // GraphQL query operation: User
8 | // ====================================================
9 |
10 | export interface User_user_bookings_result_listing {
11 | __typename: "Listing";
12 | id: string;
13 | title: string;
14 | image: string;
15 | address: string;
16 | price: number;
17 | numOfGuests: number;
18 | }
19 |
20 | export interface User_user_bookings_result {
21 | __typename: "Booking";
22 | id: string;
23 | listing: User_user_bookings_result_listing;
24 | checkIn: string;
25 | checkOut: string;
26 | }
27 |
28 | export interface User_user_bookings {
29 | __typename: "Bookings";
30 | total: number;
31 | result: User_user_bookings_result[];
32 | }
33 |
34 | export interface User_user_listings_result {
35 | __typename: "Listing";
36 | id: string;
37 | title: string;
38 | image: string;
39 | address: string;
40 | price: number;
41 | numOfGuests: number;
42 | }
43 |
44 | export interface User_user_listings {
45 | __typename: "Listings";
46 | total: number;
47 | result: User_user_listings_result[];
48 | }
49 |
50 | export interface User_user {
51 | __typename: "User";
52 | id: string;
53 | name: string;
54 | avatar: string;
55 | contact: string;
56 | hasWallet: boolean;
57 | income: number | null;
58 | bookings: User_user_bookings | null;
59 | listings: User_user_listings;
60 | }
61 |
62 | export interface User {
63 | user: User_user;
64 | }
65 |
66 | export interface UserVariables {
67 | id: string;
68 | bookingsPage: number;
69 | listingsPage: number;
70 | limit: number;
71 | }
72 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/queries/User/index.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-boost';
2 |
3 | export const USER = gql`
4 | query User($id: ID!, $bookingsPage: Int!, $listingsPage: Int!, $limit: Int!) {
5 | user(id: $id) {
6 | id
7 | name
8 | avatar
9 | contact
10 | hasWallet
11 | income
12 | bookings(limit: $limit, page: $bookingsPage) {
13 | total
14 | result {
15 | id
16 | listing {
17 | id
18 | title
19 | image
20 | address
21 | price
22 | numOfGuests
23 | }
24 | checkIn
25 | checkOut
26 | }
27 | }
28 | listings(limit: $limit, page: $listingsPage) {
29 | total
30 | result {
31 | id
32 | title
33 | image
34 | address
35 | price
36 | numOfGuests
37 | }
38 | }
39 | }
40 | }
41 | `;
42 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/graphql/queries/index.ts:
--------------------------------------------------------------------------------
1 | export * from './AuthUrl';
2 | export * from './Listing';
3 | export * from './Listings';
4 | export * from './User';
5 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useScrollToTop';
2 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/hooks/useScrollToTop/index.ts:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect } from 'react';
2 |
3 | export const useScrollToTop = () => {
4 | useLayoutEffect(() => {
5 | window.scrollTo(0, 0);
6 | }, []);
7 | };
8 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | export interface IViewer {
2 | id: string | null;
3 | token: string | null;
4 | avatar: string | null;
5 | hasWallet: boolean | null;
6 | didRequest: boolean;
7 | }
8 |
--------------------------------------------------------------------------------
/app2/app/client/src/lib/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { message, notification } from 'antd';
2 |
3 | export const iconColor = '#1890ff';
4 |
5 | export const formatListingPrice = (price: number, round = true) => {
6 | const formattedListingPrice = round ? Math.round(price / 100) : price / 100;
7 | return `$${formattedListingPrice}`;
8 | };
9 |
10 | export const displaySuccessNotification = (
11 | message: string,
12 | description?: string
13 | ) => {
14 | return notification['success']({
15 | message,
16 | description,
17 | placement: 'topLeft',
18 | style: {
19 | marginTop: 50,
20 | },
21 | });
22 | };
23 |
24 | export const displayErrorMessage = (error: string) => {
25 | return message.error(error);
26 | };
27 |
--------------------------------------------------------------------------------
/app2/app/client/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/AppHeader/assets/tinyhouse-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app2/app/client/src/sections/AppHeader/assets/tinyhouse-logo.png
--------------------------------------------------------------------------------
/app2/app/client/src/sections/AppHeader/components/MenuItems/index.tsx:
--------------------------------------------------------------------------------
1 | import { HomeOutlined, LogoutOutlined, UserOutlined } from '@ant-design/icons';
2 | import { useMutation } from '@apollo/client';
3 | import { Avatar, Button, Menu } from 'antd';
4 | import { LOG_OUT } from 'lib/graphql/mutations';
5 | import { LogOut as LogOutData } from 'lib/graphql/mutations/LogOut/__generated__/LogOut';
6 | import { IViewer } from 'lib/types';
7 | import { displayErrorMessage, displaySuccessNotification } from 'lib/utils';
8 | import { Link } from 'react-router-dom';
9 |
10 | interface IProps {
11 | viewer: IViewer;
12 | setViewer: (viewer: IViewer) => void;
13 | }
14 |
15 | const { Item, SubMenu } = Menu;
16 |
17 | export const MenuItems = ({ viewer, setViewer }: IProps) => {
18 | const [logOut] = useMutation(LOG_OUT, {
19 | onCompleted: (data) => {
20 | if (data && data.logOut) {
21 | setViewer(data.logOut);
22 | sessionStorage.removeItem('token');
23 | displaySuccessNotification("You'e successfully logged out!");
24 | }
25 | },
26 | onError: (data) => {
27 | displayErrorMessage(
28 | "Sorry! We weren't able to log you out. Please try again later!"
29 | );
30 | },
31 | });
32 |
33 | const hanleLogOut = () => {
34 | logOut();
35 | };
36 | const subMenuLogin =
37 | viewer.id && viewer.avatar ? (
38 | }>
39 | -
40 |
41 |
42 | Profile
43 |
44 |
45 | -
46 |
47 |
48 | Log out
49 |
50 |
51 |
52 | ) : (
53 | -
54 |
55 | Sign In
56 |
57 |
58 | );
59 |
60 | return (
61 |
62 | -
63 |
64 | Host
65 |
66 |
67 | {subMenuLogin}
68 |
69 | );
70 | };
71 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/AppHeader/components/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './MenuItems';
2 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/AppHeader/index.tsx:
--------------------------------------------------------------------------------
1 | import { Input, Layout } from 'antd';
2 | import { IViewer } from 'lib/types';
3 | import { displayErrorMessage } from 'lib/utils';
4 | import { useEffect, useState } from 'react';
5 | import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
6 | import logo from './assets/tinyhouse-logo.png';
7 | import { MenuItems } from './components';
8 |
9 | interface Props {
10 | viewer: IViewer;
11 | setViewer: (viewer: IViewer) => void;
12 | }
13 |
14 | const { Header } = Layout;
15 | const { Search } = Input;
16 |
17 | export const AppHeader = withRouter(
18 | ({
19 | viewer,
20 | setViewer,
21 | location,
22 | match,
23 | history,
24 | }: Props & RouteComponentProps) => {
25 | const [search, setSearch] = useState('');
26 |
27 | useEffect(() => {
28 | const { pathname } = location;
29 | const pathnameSubStrings = pathname.split('/');
30 |
31 | if (!pathname.includes('/listings')) {
32 | setSearch('');
33 | return;
34 | }
35 |
36 | if (pathname.includes('/listings') && pathnameSubStrings.length === 3) {
37 | setSearch(pathnameSubStrings[2]);
38 | return;
39 | }
40 | }, [location]);
41 |
42 | const onSearch = (value: string) => {
43 | const trimmedValue = value.trim();
44 |
45 | if (trimmedValue) {
46 | history.push(`/listings/${trimmedValue}`);
47 | } else {
48 | displayErrorMessage('Please enter a valid search!');
49 | }
50 | };
51 |
52 | return (
53 |
74 | );
75 | }
76 | );
77 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Home/assets/cancun.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app2/app/client/src/sections/Home/assets/cancun.jpg
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Home/assets/dubai.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app2/app/client/src/sections/Home/assets/dubai.jpg
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Home/assets/listing-loading-card-cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app2/app/client/src/sections/Home/assets/listing-loading-card-cover.jpg
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Home/assets/london.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app2/app/client/src/sections/Home/assets/london.jpg
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Home/assets/los-angeles.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app2/app/client/src/sections/Home/assets/los-angeles.jpg
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Home/assets/map-background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app2/app/client/src/sections/Home/assets/map-background.jpg
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Home/assets/san-fransisco.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app2/app/client/src/sections/Home/assets/san-fransisco.jpg
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Home/assets/toronto.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app2/app/client/src/sections/Home/assets/toronto.jpg
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Home/components/HomeHero/index.tsx:
--------------------------------------------------------------------------------
1 | import { Card, Col, Input, Row, Typography } from 'antd';
2 | import { Link } from 'react-router-dom';
3 | import dubaiImage from '../../assets/dubai.jpg';
4 | import londonImage from '../../assets/london.jpg';
5 | import losAngelesImage from '../../assets/los-angeles.jpg';
6 | import torontoImage from '../../assets/toronto.jpg';
7 |
8 | require('dotenv').config();
9 |
10 | const { Title } = Typography;
11 | const { Search } = Input;
12 |
13 | interface Props {
14 | onSearch: (value: string) => void;
15 | }
16 |
17 | export const HomeHero = ({ onSearch }: Props) => {
18 | return (
19 |
20 |
21 |
22 | Find a place you'll love to stay at
23 |
24 |
31 |
32 |
33 |
34 |
35 | }>
36 | Toronto
37 |
38 |
39 |
40 |
41 |
42 | }>Dubai
43 |
44 |
45 |
46 |
47 | }>
48 | Los Angeles
49 |
50 |
51 |
52 |
53 |
54 | }>London
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Home/components/HomeListings/index.tsx:
--------------------------------------------------------------------------------
1 | import { List, Typography } from 'antd';
2 | import { ListingCard } from 'lib/components';
3 | import { Listings } from 'lib/graphql/queries/Listings/__generated__/Listings';
4 |
5 | interface Props {
6 | title: string;
7 | listings: Listings['listings']['result'];
8 | }
9 |
10 | const { Title } = Typography;
11 |
12 | export const HomeListings = ({ title, listings }: Props) => {
13 | return (
14 |
15 |
16 | {title}
17 |
18 | (
27 |
28 |
29 |
30 | )}
31 | />
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Home/components/HomeListingsSkeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import { Card, List, Skeleton } from 'antd';
2 | import listingLoadingCardCover from '../../assets/listing-loading-card-cover.jpg';
3 |
4 | export const HomeListingsSkeleton = () => {
5 | const emptyData = [{}, {}, {}, {}];
6 | return (
7 |
8 |
9 | (
18 |
19 |
25 | }
26 | loading
27 | />
28 |
29 | )}
30 | />
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Home/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './HomeHero';
2 | export * from './HomeListings';
3 | export * from './HomeListingsSkeleton';
4 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Home/index.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import { Col, Layout, Row, Typography } from 'antd';
3 | import { ListingsFilter } from 'lib/graphql/globalTypes';
4 | import { LISTINGS } from 'lib/graphql/queries';
5 | import {
6 | Listings as ListingsData,
7 | ListingsVariables,
8 | } from 'lib/graphql/queries/Listings/__generated__/Listings';
9 | import { useScrollToTop } from 'lib/hooks';
10 | import { displayErrorMessage } from 'lib/utils';
11 | import { Link, RouteComponentProps } from 'react-router-dom';
12 | import {
13 | HomeHero,
14 | HomeListings,
15 | HomeListingsSkeleton,
16 | } from 'sections/Home/components';
17 | import cancunImage from './assets/cancun.jpg';
18 | import mapBackground from './assets/map-background.jpg';
19 | import sanFransiscoImage from './assets/san-fransisco.jpg';
20 |
21 | const { Content } = Layout;
22 | const { Paragraph, Title } = Typography;
23 |
24 | const PAGE_LIMIT = 4;
25 | const PAGE_NUMBER = 1;
26 |
27 | export const Home = ({ history }: RouteComponentProps) => {
28 | const { loading, data } = useQuery(
29 | LISTINGS,
30 | {
31 | variables: {
32 | filter: ListingsFilter.PRICE_HIGH_TO_LOW,
33 | limit: PAGE_LIMIT,
34 | page: PAGE_NUMBER,
35 | },
36 | fetchPolicy: 'cache-and-network',
37 | }
38 | );
39 |
40 | useScrollToTop();
41 |
42 | const onSearch = (value: string) => {
43 | const trimmedValue = value.trim();
44 | if (trimmedValue) {
45 | history.push(`/listings/${trimmedValue}`);
46 | } else {
47 | displayErrorMessage('Please enter a valid search!');
48 | }
49 | };
50 |
51 | const renderListingsSection = () => {
52 | if (loading) {
53 | return ;
54 | }
55 |
56 | if (data) {
57 | return (
58 |
62 | );
63 | }
64 |
65 | return null;
66 | };
67 |
68 | return (
69 |
73 |
74 |
75 |
76 | Your guide for all things rental
77 |
78 |
79 | Helping you make the best decisions in renting your last minute
80 | locations.
81 |
82 |
86 | Popular listings in the United States
87 |
88 |
89 |
90 | {renderListingsSection()}
91 |
92 |
93 |
94 | Listings of any kind
95 |
96 |
97 |
98 |
99 |
100 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | );
123 | };
124 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Listing/components/ListingBookings/index.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, Divider, List, Typography } from 'antd';
2 | import { Listing } from 'lib/graphql/queries/Listing/__generated__/Listing';
3 | import { Link } from 'react-router-dom';
4 |
5 | interface IProps {
6 | listingBookings: Listing['listing']['bookings'];
7 | bookingsPage: number;
8 | limit: number;
9 | setBookingsPage: (page: number) => void;
10 | }
11 |
12 | const { Text, Title } = Typography;
13 |
14 | export const ListingBookings = ({
15 | listingBookings,
16 | bookingsPage,
17 | limit,
18 | setBookingsPage,
19 | }: IProps) => {
20 | const total = listingBookings ? listingBookings.total : null;
21 | const result = listingBookings ? listingBookings.result : null;
22 |
23 | const listingBookingsList = listingBookings ? (
24 | setBookingsPage(page),
40 | }}
41 | renderItem={(listingBooking) => {
42 | const bookingHistory = (
43 |
44 |
45 | Check in: {listingBooking.checkIn}
46 |
47 |
48 | Check out: {listingBooking.checkOut}
49 |
50 |
51 | );
52 | return (
53 |
54 | {bookingHistory}
55 |
56 |
57 |
62 |
63 |
64 | );
65 | }}
66 | />
67 | ) : null;
68 |
69 | const listingBookingElement = listingBookingsList ? (
70 |
71 |
72 |
73 |
Bookings
74 |
75 |
76 | {listingBookingsList}
77 |
78 | ) : null;
79 |
80 | return listingBookingElement;
81 | };
82 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Listing/components/ListingCreateBooking/types.ts:
--------------------------------------------------------------------------------
1 | interface IBookingsIndexMonth {
2 | [key: string]: boolean;
3 | }
4 |
5 | interface IBookingsIndexYear {
6 | [key: string]: IBookingsIndexMonth;
7 | }
8 |
9 | export interface IBookingsIndex {
10 | [key: string]: IBookingsIndexYear;
11 | }
12 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Listing/components/ListingDetails/index.tsx:
--------------------------------------------------------------------------------
1 | import { EnvironmentOutlined } from '@ant-design/icons';
2 | import { Avatar, Divider, Tag, Typography } from 'antd';
3 | import { Listing as ListingData } from 'lib/graphql/queries/Listing/__generated__/Listing';
4 | import { iconColor } from 'lib/utils';
5 | import { Link } from 'react-router-dom';
6 |
7 | interface IProps {
8 | listing: ListingData['listing'];
9 | }
10 |
11 | const { Paragraph, Title } = Typography;
12 |
13 | export const ListingDetails = ({ listing }: IProps) => {
14 | const { title, description, image, type, address, city, numOfGuests, host } =
15 | listing;
16 |
17 | return (
18 |
19 |
23 |
24 |
25 |
30 |
31 | {city}
32 |
33 |
34 | {address}
35 |
36 |
37 | {title}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {host.name}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
About this space
56 |
57 | {type}
58 | {numOfGuests} Guests
59 |
60 |
61 | {description}
62 |
63 |
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Listing/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ListingBookings';
2 | export * from './ListingCreateBooking';
3 | export * from './ListingCreateBookingModal';
4 | export * from './ListingDetails';
5 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Listing/index.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import { Col, Layout, Row } from 'antd';
3 | import { ErrorBanner, PageSkeleton } from 'lib/components';
4 | import { LISTING } from 'lib/graphql/queries';
5 | import {
6 | Listing as ListingData,
7 | ListingVariables,
8 | } from 'lib/graphql/queries/Listing/__generated__/Listing';
9 | import { useScrollToTop } from 'lib/hooks';
10 | import { IViewer } from 'lib/types';
11 | import { Moment } from 'moment';
12 | import { useState } from 'react';
13 | import { RouteComponentProps } from 'react-router-dom';
14 | import {
15 | ListingBookings,
16 | ListingCreateBooking,
17 | ListingDetails,
18 | WrappedListingCreateBookingModal as ListingCreateBookingModal,
19 | } from 'sections/Listing/components';
20 |
21 | interface IProps {
22 | viewer: IViewer;
23 | }
24 | interface IMatchParams {
25 | id: string;
26 | }
27 |
28 | const { Content } = Layout;
29 | const PAGE_LIMIT = 3;
30 |
31 | export const Listing = ({
32 | viewer,
33 | match,
34 | }: IProps & RouteComponentProps) => {
35 | const [bookingsPage, setBookingsPage] = useState(1);
36 | const [checkInDate, setCheckInDate] = useState(null);
37 | const [checkOutDate, setCheckOutDate] = useState(null);
38 | const [modalVisible, setModalVisible] = useState(false);
39 |
40 | const { loading, data, error, refetch } = useQuery<
41 | ListingData,
42 | ListingVariables
43 | >(LISTING, {
44 | variables: {
45 | id: match.params.id,
46 | bookingsPage,
47 | limit: PAGE_LIMIT,
48 | },
49 | });
50 |
51 | useScrollToTop();
52 |
53 | const clearBookingData = () => {
54 | setModalVisible(false);
55 | setCheckInDate(null);
56 | setCheckOutDate(null);
57 | };
58 |
59 | const handleListingRefetch = async () => {
60 | await refetch();
61 | };
62 |
63 | if (loading) {
64 | return (
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | if (error) {
72 | return (
73 |
74 |
75 |
76 |
77 | );
78 | }
79 |
80 | const listing = data ? data.listing : null;
81 | const listingBookings = listing ? listing.bookings : null;
82 |
83 | const listingDetailsElement = listing ? (
84 |
85 | ) : null;
86 |
87 | const listingBookingsElement = listingBookings ? (
88 |
94 | ) : null;
95 |
96 | const listingCreateBookingElement = listing ? (
97 |
108 | ) : null;
109 |
110 | const listingCreateBookingModalElement =
111 | listing && checkInDate && checkOutDate ? (
112 |
122 | ) : null;
123 |
124 | return (
125 |
126 |
127 |
128 | {listingDetailsElement}
129 | {listingBookingsElement}
130 |
131 |
132 | {listingCreateBookingElement}
133 |
134 |
135 | {listingCreateBookingModalElement}
136 |
137 | );
138 | };
139 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Listings/assets/listing-loading-card-cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app2/app/client/src/sections/Listings/assets/listing-loading-card-cover.jpg
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Listings/components/ListingsFilters/index.tsx:
--------------------------------------------------------------------------------
1 | import { Select } from 'antd';
2 | import { ListingsFilter } from 'lib/graphql/globalTypes';
3 |
4 | interface Props {
5 | filter: ListingsFilter;
6 | setFilter: (filter: ListingsFilter) => void;
7 | }
8 |
9 | const { Option } = Select;
10 |
11 | export const ListingsFilters = ({ filter, setFilter }: Props) => {
12 | return (
13 |
14 | Filter By
15 | setFilter(filter)}
18 | >
19 |
20 | Price: Low to High
21 |
22 |
23 | Price: High to Low
24 |
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Listings/components/ListingsPagination/index.tsx:
--------------------------------------------------------------------------------
1 | import { Pagination } from 'antd';
2 |
3 | interface IProps {
4 | total: number;
5 | page: number;
6 | limit: number;
7 | setPage: (page: number) => void;
8 | }
9 |
10 | export const ListingsPagination = ({ total, page, limit, setPage }: IProps) => {
11 | return (
12 | setPage(page)}
19 | className="listings-pagination"
20 | />
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Listings/components/ListingsSkeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import { Card, List, Skeleton } from 'antd';
2 | import listingLoadingCardCover from '../../assets/listing-loading-card-cover.jpg';
3 |
4 | export const ListingsSkeleton = () => {
5 | const emptyData = [{}, {}, {}, {}, {}, {}, {}, {}];
6 | return (
7 |
8 |
9 | (
18 |
19 |
25 | }
26 | loading
27 | className="listings-skeleton__card"
28 | />
29 |
30 | )}
31 | />
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Listings/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ListingsFilters';
2 | export * from './ListingsPagination';
3 | export * from './ListingsSkeleton';
4 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Listings/index.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import { Affix, Layout, List, Typography } from 'antd';
3 | import { ErrorBanner, ListingCard } from 'lib/components';
4 | import { ListingsFilter } from 'lib/graphql/globalTypes';
5 | import { LISTINGS } from 'lib/graphql/queries';
6 | import {
7 | Listings as ListingsData,
8 | ListingsVariables,
9 | } from 'lib/graphql/queries/Listings/__generated__/Listings';
10 | import { useScrollToTop } from 'lib/hooks';
11 | import { useEffect, useRef, useState } from 'react';
12 | import { Link, RouteComponentProps } from 'react-router-dom';
13 | import {
14 | ListingsFilters,
15 | ListingsPagination,
16 | ListingsSkeleton,
17 | } from 'sections/Listings/components';
18 |
19 | interface IMatchParams {
20 | location: string;
21 | }
22 |
23 | const { Content } = Layout;
24 | const { Paragraph, Text, Title } = Typography;
25 |
26 | const PAGE_LIMIT = 8;
27 |
28 | export const Listings = ({ match }: RouteComponentProps) => {
29 | const locationRef = useRef(match.params.location);
30 | const [filter, setFilter] = useState(ListingsFilter.PRICE_LOW_TO_HIGH);
31 |
32 | const [page, setPage] = useState(1);
33 |
34 | const { loading, data, error } = useQuery(
35 | LISTINGS,
36 | {
37 | skip: locationRef.current !== match.params.location && page !== 1,
38 | variables: {
39 | location: match.params.location,
40 | filter,
41 | limit: PAGE_LIMIT,
42 | page,
43 | },
44 | }
45 | );
46 |
47 | useScrollToTop();
48 |
49 | useEffect(() => {
50 | setPage(1);
51 | locationRef.current = match.params.location;
52 | }, [match.params.location]);
53 |
54 | if (loading) {
55 | return (
56 |
57 |
58 |
59 | );
60 | }
61 |
62 | if (error) {
63 | return (
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | const listings = data ? data.listings : null;
72 | const listingsRegion = listings ? listings.region : null;
73 |
74 | const listingsSectionElement =
75 | listings && listings.result.length ? (
76 |
77 |
78 |
84 |
85 |
86 |
(
95 |
96 |
97 |
98 | )}
99 | />
100 |
101 | ) : (
102 |
103 |
104 | It appears that no listings have yet been created for{' '}
105 | "{listingsRegion}" {' '}
106 |
107 |
108 | Be the first person to create a{' '}
109 | listing in this area !
110 |
111 |
112 | );
113 |
114 | const listingsRegionElement = listingsRegion ? (
115 |
116 | Results for "{listingsRegion}"
117 |
118 | ) : null;
119 |
120 | return (
121 |
122 | {listingsRegionElement}
123 | {listingsSectionElement}
124 |
125 | );
126 | };
127 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Login/assets/google_logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/app2/app/client/src/sections/Login/assets/google_logo.jpg
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Login/index.tsx:
--------------------------------------------------------------------------------
1 | import { useApolloClient, useMutation } from '@apollo/client';
2 | import { Card, Layout, Spin, Typography } from 'antd';
3 | import { ErrorBanner } from 'lib/components';
4 | import { LOG_IN } from 'lib/graphql/mutations';
5 | import {
6 | LogIn as LogInData,
7 | LogInVariables,
8 | } from 'lib/graphql/mutations/LogIn/__generated__/LogIn';
9 | import { AUTH_URL } from 'lib/graphql/queries/AuthUrl';
10 | import { AuthUrl as AuthUrlData } from 'lib/graphql/queries/AuthUrl/__generated__/AuthUrl';
11 | import { useScrollToTop } from 'lib/hooks';
12 | import { IViewer } from 'lib/types';
13 | import { displayErrorMessage, displaySuccessNotification } from 'lib/utils';
14 | import { useEffect, useRef } from 'react';
15 | import { Redirect } from 'react-router-dom';
16 | import googleLogo from './assets/google_logo.jpg';
17 |
18 | interface Props {
19 | setViewer: (viewer: IViewer) => void;
20 | }
21 |
22 | const { Content } = Layout;
23 | const { Text, Title } = Typography;
24 |
25 | export const Login = ({ setViewer }: Props) => {
26 | const client = useApolloClient();
27 |
28 | const [
29 | logIn,
30 | { data: logInData, loading: logInLoading, error: logInError },
31 | ] = useMutation(LOG_IN, {
32 | onCompleted: (data) => {
33 | if (data && data.logIn && data.logIn.token) {
34 | setViewer(data.logIn);
35 | sessionStorage.setItem('token', data.logIn.token);
36 | displaySuccessNotification("You've successfully logged in!");
37 | }
38 | },
39 | });
40 |
41 | const logInRef = useRef(logIn);
42 |
43 | useScrollToTop();
44 |
45 | useEffect(() => {
46 | const code = new URL(window.location.href).searchParams.get('code');
47 |
48 | if (code) {
49 | logInRef.current({
50 | variables: {
51 | input: { code },
52 | },
53 | });
54 | }
55 | }, []);
56 |
57 | const handleAuthorize = async () => {
58 | try {
59 | const { data } = await client.query({
60 | query: AUTH_URL,
61 | });
62 |
63 | if (data) {
64 | window.location.href = data.authUrl;
65 | }
66 | } catch {
67 | displayErrorMessage(
68 | "Sorry! We weren't able to log you in. Please try again later!"
69 | );
70 | }
71 | };
72 |
73 | if (logInLoading) {
74 | return (
75 |
76 |
77 |
78 | );
79 | }
80 |
81 | if (logInData && logInData.logIn) {
82 | const { id: viewerId } = logInData.logIn;
83 | return ;
84 | }
85 |
86 | const logInErrorBannerElement = logInError ? (
87 |
88 | ) : null;
89 |
90 | return (
91 |
92 | {logInErrorBannerElement}
93 |
94 |
95 |
96 |
97 | 💥
98 |
99 |
100 |
101 | Log in to TinyHouse!
102 |
103 |
104 | Sign in with Google to start booking available rentals!
105 |
106 |
107 |
111 |
116 |
117 | Sign in with Google
118 |
119 |
120 |
121 | Note: By signing in, you'll be redirected to the Google consent form
122 | to sign in with your Google account.
123 |
124 |
125 |
126 | );
127 | };
128 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/NotFound/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Empty, Layout, Typography } from 'antd';
3 | import { Link } from 'react-router-dom';
4 |
5 | const { Content } = Layout;
6 | const { Text } = Typography;
7 |
8 | export const NotFound = () => {
9 | return (
10 |
11 |
14 |
15 | Uh oh! Something went wrong :(
16 |
17 |
18 | The page you're looking for can't be found
19 |
20 |
21 | }
22 | />
23 |
27 | Go to home
28 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/Stripe/index.tsx:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import { Layout, Spin } from 'antd';
3 | import { CONNECT_STRIPE } from 'lib/graphql/mutations';
4 | import {
5 | ConnectStripe as ConnectStripeData,
6 | ConnectStripeVariables,
7 | } from 'lib/graphql/mutations/ConnectStripe/__generated__/ConnectStripe';
8 | import { useScrollToTop } from 'lib/hooks';
9 | import { IViewer } from 'lib/types';
10 | import { displaySuccessNotification } from 'lib/utils';
11 | import { useEffect, useRef } from 'react';
12 | import { Redirect, RouteComponentProps } from 'react-router-dom';
13 |
14 | interface IProps {
15 | viewer: IViewer;
16 | setViewer: (viewer: IViewer) => void;
17 | }
18 |
19 | const { Content } = Layout;
20 |
21 | export const Stripe = ({
22 | viewer,
23 | setViewer,
24 | history,
25 | }: IProps & RouteComponentProps) => {
26 | const [connectStripe, { data, loading, error }] = useMutation<
27 | ConnectStripeData,
28 | ConnectStripeVariables
29 | >(CONNECT_STRIPE, {
30 | onCompleted: (data) => {
31 | if (data && data.connectStripe) {
32 | setViewer({ ...viewer, hasWallet: data.connectStripe.hasWallet });
33 | displaySuccessNotification(
34 | "You've successfully connected your Stripe Account!",
35 | 'You can now begin to create listings in the Host page'
36 | );
37 | }
38 | },
39 | });
40 |
41 | const connectStripeRef = useRef(connectStripe);
42 |
43 | useScrollToTop();
44 |
45 | useEffect(() => {
46 | const code = new URL(window.location.href).searchParams.get('code');
47 | if (code) {
48 | connectStripeRef.current({
49 | variables: {
50 | input: { code },
51 | },
52 | });
53 | } else {
54 | history.replace('/login');
55 | }
56 | }, [history]);
57 |
58 | if (data && data.connectStripe) {
59 | return ;
60 | }
61 |
62 | if (loading) {
63 | return (
64 |
65 |
66 |
67 | );
68 | }
69 |
70 | if (error) {
71 | return ;
72 | }
73 |
74 | return null;
75 | };
76 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/User/components/UserBookings/index.tsx:
--------------------------------------------------------------------------------
1 | import { List, Typography } from 'antd';
2 | import { ListingCard } from 'lib/components';
3 | import { User } from 'lib/graphql/queries/User/__generated__/User';
4 |
5 | interface IProps {
6 | userBookings: User['user']['bookings'];
7 | bookingsPage: number;
8 | limit: number;
9 | setBookingsPage: (page: number) => void;
10 | }
11 |
12 | const { Paragraph, Text, Title } = Typography;
13 |
14 | export const UserBookings = ({
15 | userBookings,
16 | bookingsPage,
17 | limit,
18 | setBookingsPage,
19 | }: IProps) => {
20 | const total = userBookings ? userBookings.total : null;
21 | const result = userBookings ? userBookings.result : null;
22 |
23 | const userBookingsList = userBookings ? (
24 | setBookingsPage(page),
41 | }}
42 | renderItem={(userBooking) => {
43 | const bookingHistory = (
44 |
45 |
46 | Check in: {userBooking.checkIn}
47 |
48 |
49 | Check out: {userBooking.checkOut}
50 |
51 |
52 | );
53 | return (
54 |
55 | {bookingHistory}
56 |
57 |
58 | );
59 | }}
60 | />
61 | ) : null;
62 |
63 | const userBookingElement = userBookingsList ? (
64 |
65 |
66 | Bookings
67 |
68 |
69 | This section highlights the bookings you've made, and the
70 | check-in/check-out dates associated with said bookings.
71 |
72 | {userBookingsList}
73 |
74 | ) : null;
75 |
76 | return userBookingElement;
77 | };
78 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/User/components/UserListings/index.tsx:
--------------------------------------------------------------------------------
1 | import { List, Typography } from 'antd';
2 | import { ListingCard } from 'lib/components';
3 | import { User } from 'lib/graphql/queries/User/__generated__/User';
4 |
5 | interface Props {
6 | userListings: User['user']['listings'];
7 | listingsPage: number;
8 | limit: number;
9 | setListingsPage: (page: number) => void;
10 | }
11 |
12 | const { Paragraph, Title } = Typography;
13 |
14 | export const UserListings = ({
15 | userListings,
16 | listingsPage,
17 | limit,
18 | setListingsPage,
19 | }: Props) => {
20 | const { total, result } = userListings;
21 |
22 | const userListingsList = (
23 | setListingsPage(page),
40 | }}
41 | renderItem={(userListing) => (
42 |
43 |
44 |
45 | )}
46 | />
47 | );
48 |
49 | return (
50 |
51 |
52 | Listings
53 |
54 |
55 | This section highlights the listings this user currently hosts and has
56 | made available for bookings.
57 |
58 | {userListingsList}
59 |
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/User/components/UserProfile/index.tsx:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@apollo/client';
2 | import { Avatar, Button, Card, Divider, Tag, Typography } from 'antd';
3 | import { DISCONNECT_STRIPE } from 'lib/graphql/mutations';
4 | import { DisconnectStripe as DisconnectStripeData } from 'lib/graphql/mutations/DisconnectStripe/__generated__/DisconnectStripe';
5 | import { User as UserData } from 'lib/graphql/queries/User/__generated__/User';
6 | import { IViewer } from 'lib/types';
7 | import {
8 | displayErrorMessage,
9 | displaySuccessNotification,
10 | formatListingPrice,
11 | } from 'lib/utils';
12 | import { Fragment } from 'react';
13 |
14 | interface IProps {
15 | user: UserData['user'];
16 | viewer: IViewer;
17 | viewerIsUser: boolean;
18 | setViewer: (viewer: IViewer) => void;
19 | handleUserRefetch: () => Promise;
20 | }
21 |
22 | const stripeAuthUrl = `https://connect.stripe.com/oauth/authorize?response_type=code&client_id=${process.env.REACT_APP_STRIPE_CONNECT_CLIENT_ID}&scope=read_write&redirect_uri=https://tinyhouse.dev`;
23 |
24 | const { Paragraph, Text, Title } = Typography;
25 |
26 | export const UserProfile = ({
27 | user,
28 | viewer,
29 | viewerIsUser,
30 | setViewer,
31 | handleUserRefetch,
32 | }: IProps) => {
33 | const [disconnectStripe, { loading }] = useMutation(
34 | DISCONNECT_STRIPE,
35 | {
36 | onCompleted: (data) => {
37 | if (data && data.disconnectStripe) {
38 | setViewer({ ...viewer, hasWallet: data.disconnectStripe.hasWallet });
39 | displaySuccessNotification(
40 | "You've successfully disconnected from Stripe!",
41 | "You'll have to reconnect with Stripe to continue to create listings"
42 | );
43 | handleUserRefetch();
44 | }
45 | },
46 | onError: () => {
47 | displayErrorMessage(
48 | "Sorry! We were't able to disconnect you from Stripe, Please try again later!"
49 | );
50 | },
51 | }
52 | );
53 |
54 | const redirectToStripe = () => {
55 | window.location.href = stripeAuthUrl;
56 | };
57 |
58 | const additionalDetails = user.hasWallet ? (
59 |
60 |
61 | Stripe Registered
62 |
63 |
64 | Income Earned:{' '}
65 |
66 | {user.income ? formatListingPrice(user.income) : `$0`}
67 |
68 |
69 | disconnectStripe()}
74 | >
75 | Disconnect Stripe
76 |
77 |
78 | By disconnecting, you won't be able to receive{' '}
79 | any further payments . This will prevent users from
80 | booking listings that you might have already created.
81 |
82 |
83 | ) : (
84 |
85 |
86 | Interested in becoming a TinyHouse host? Register with your Stripe
87 | account!
88 |
89 |
94 | Connect with Stripe
95 |
96 |
97 | TinyHouse uses{' '}
98 |
103 | Stripe
104 | {' '}
105 | to help transfer your earnings in a secure and truster manner
106 |
107 |
108 | );
109 |
110 | const additionalDetailsSection = viewerIsUser ? (
111 |
112 |
113 |
114 |
Additional Details
115 | {additionalDetails}
116 |
117 |
118 | ) : null;
119 |
120 | return (
121 |
122 |
123 |
126 |
127 |
128 |
Details
129 |
130 | Name: {user.name}
131 |
132 |
133 | Contact: {user.contact}
134 |
135 |
136 | {additionalDetailsSection}
137 |
138 |
139 | );
140 | };
141 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/User/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './UserProfile';
2 | export * from './UserBookings';
3 | export * from './UserListings';
4 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/User/index.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@apollo/client';
2 | import { Col, Layout, Row } from 'antd';
3 | import { ErrorBanner, PageSkeleton } from 'lib/components';
4 | import { USER } from 'lib/graphql/queries';
5 | import {
6 | User as UserData,
7 | UserVariables,
8 | } from 'lib/graphql/queries/User/__generated__/User';
9 | import { useScrollToTop } from 'lib/hooks';
10 | import { IViewer } from 'lib/types';
11 | import { useState } from 'react';
12 | import { RouteComponentProps } from 'react-router-dom';
13 | import { UserBookings, UserListings, UserProfile } from './components';
14 |
15 | interface IProps {
16 | viewer: IViewer;
17 | setViewer: (viewer: IViewer) => void;
18 | }
19 |
20 | interface IMatchParams {
21 | id: string;
22 | }
23 |
24 | const { Content } = Layout;
25 | const PAGE_LIMIT = 4;
26 |
27 | export const User = ({
28 | viewer,
29 | setViewer,
30 | match,
31 | }: IProps & RouteComponentProps) => {
32 | const [listingsPage, setListingsPage] = useState(1);
33 | const [bookingsPage, setBookingsPage] = useState(1);
34 |
35 | const { data, loading, error, refetch } = useQuery(
36 | USER,
37 | {
38 | variables: {
39 | id: match.params.id,
40 | bookingsPage,
41 | listingsPage,
42 | limit: PAGE_LIMIT,
43 | },
44 | fetchPolicy: 'cache-and-network',
45 | }
46 | );
47 |
48 | useScrollToTop();
49 |
50 | const handleUserRefetch = async () => {
51 | await refetch();
52 | };
53 |
54 | const stripeError = new URL(window.location.href).searchParams.get(
55 | 'stripe_error'
56 | );
57 | const stripeErrorBanner = stripeError ? (
58 |
59 | ) : null;
60 |
61 | if (loading) {
62 | return (
63 |
64 |
65 |
66 | );
67 | }
68 |
69 | if (error) {
70 | return (
71 |
72 |
73 |
74 |
75 | );
76 | }
77 |
78 | const user = data ? data.user : null;
79 | const viewerIsUser = viewer.id === match.params.id;
80 |
81 | const userListings = user ? user.listings : null;
82 | const userBookings = user ? user.bookings : null;
83 |
84 | const userProfileElement = user ? (
85 |
92 | ) : null;
93 |
94 | const userListingsElement = userListings ? (
95 |
101 | ) : null;
102 |
103 | const userBookingsElement = userBookings ? (
104 |
110 | ) : null;
111 |
112 | return (
113 |
114 | s user may Row {stripeErrorBanner}
115 |
116 | {userProfileElement}
117 |
118 | {userListingsElement} {userBookingsElement}
119 |
120 |
121 |
122 | );
123 | };
124 |
--------------------------------------------------------------------------------
/app2/app/client/src/sections/index.ts:
--------------------------------------------------------------------------------
1 | export * from './AppHeader';
2 | export * from './Home';
3 | export * from './Host';
4 | export * from './Listing';
5 | export * from './Listings';
6 | export * from './Login';
7 | export * from './NotFound';
8 | export * from './Stripe';
9 | export * from './User';
10 |
--------------------------------------------------------------------------------
/app2/app/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "target": "ES2020",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "esModuleInterop": true,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "noFallthroughCasesInSwitch": true
19 | },
20 | "include": ["src"]
21 | }
22 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.eslintignore
2 | **/.eslintrc
3 | **/.prettierignore
4 | **/.prettierrc
5 |
6 | **/node_modules/
7 | **/node_modules_linux/
8 | **/Dockerfile
9 | **/.dockerignore
10 | **/.gitignore
11 | **/.git
12 | **/README.md
13 | **/Readme.md
14 | **/LICENSE
15 | **/.vscode
16 |
17 | **/test/
18 | **/.next/
--------------------------------------------------------------------------------
/app2/app/server/mondodb/.env:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
2 | COOKIE_PARSER_SECRET=this-is-a-secret
3 | NODE_TLS_REJECT_UNAUTHORIZED=0
4 |
5 | CLOUDINARY_NAME=webmakaka
6 | CLOUDINARY_KEY=738764358432137
7 | CLOUDINARY_SECRET=uMdEieOVTmtNhDzwJpN3Dq46IiE
8 |
9 | # LOCAL DEBUGGING
10 | # GRAPHQL_PORT=3000
11 | # MONGO_URI=mongodb://localhost:27017
12 | # PUBLIC_URL=https://tinyhouse.dev
13 | # GOOGLE_CLIENT_ID=
14 | # GOOGLE_CLIENT_SECRET=
15 | # GOOGLE_GEOCODING_API_KEY=
--------------------------------------------------------------------------------
/app2/app/server/mondodb/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": { "node": true },
3 | "plugins": ["@typescript-eslint"],
4 | "parser": "@typescript-eslint/parser",
5 | "parserOptions": {
6 | "ecmaVersion": 2020,
7 | "project": "tsconfig.json"
8 | },
9 | "extends": [
10 | "eslint:recommended",
11 | "plugin:@typescript-eslint/recommended",
12 | "plugin:@typescript-eslint/recommended-requiring-type-checking"
13 | ],
14 | "rules": {
15 | "prefer-const": "error",
16 | "@typscript-eslint/no-unused-vars": "off",
17 | "@typscript-eslint/no-unused-params": "off"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules/
3 |
4 | # production
5 | build/
6 |
7 | # misc
8 | .DS_Store
9 |
10 | # environment variables
11 | #.env
12 |
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
--------------------------------------------------------------------------------
/app2/app/server/mondodb/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine3.12
2 |
3 | WORKDIR /app
4 | COPY package.json ./
5 | # RUN npm install --only=prod --silent
6 |
7 | RUN npm install --silent
8 | COPY ./ ./
9 | CMD ["npm", "run", "start"]
--------------------------------------------------------------------------------
/app2/app/server/mondodb/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | mongodb-dev:
4 | image: mongo
5 | restart: always
6 | ports:
7 | - '27017:27017'
8 | environment:
9 | MONGODB_DATABASE: mongo-database
10 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tinyhouse-server",
3 | "version": "0.1.0",
4 | "dependencies": {
5 | "@googlemaps/google-maps-services-js": "^3.1.16",
6 | "apollo-server-express": "^2.25.1",
7 | "body-parser": "^1.19.0",
8 | "cloudinary": "^1.26.0",
9 | "cookie-parser": "^1.4.5",
10 | "express": "^4.17.1",
11 | "googleapis": "^75.0.0",
12 | "graphql": "^15.5.0",
13 | "lodash.merge": "^4.6.2",
14 | "mongodb": "^3.6.9",
15 | "stripe": "^8.154.0"
16 | },
17 | "devDependencies": {
18 | "@types/body-parser": "^1.19.0",
19 | "@types/cookie-parser": "^1.4.2",
20 | "@types/express": "^4.17.12",
21 | "@types/graphql": "^14.5.0",
22 | "@types/lodash.merge": "^4.6.6",
23 | "@types/mongodb": "^3.6.17",
24 | "@types/node": "^15.12.2",
25 | "@types/stripe": "^8.0.417",
26 | "@typescript-eslint/eslint-plugin": "^4.26.1",
27 | "@typescript-eslint/parser": "^4.26.1",
28 | "dotenv": "^10.0.0",
29 | "eslint": "^7.28.0",
30 | "nodemon": "^2.0.7",
31 | "ts-node": "^10.0.0",
32 | "typescript": "^4.3.2"
33 | },
34 | "scripts": {
35 | "start": "NODE_PATH=./src nodemon src/index.ts",
36 | "seed": "NODE_PATH=./src ts-node seed/seed.ts",
37 | "clear": "NODE_PATH=./src ts-node seed/clear.ts",
38 | "build": "NODE_PATH=./src tsc -p ./",
39 | "lint": "NODE_PATH=./src eslint '**/*.ts'"
40 | },
41 | "volta": {
42 | "node": "14.15.3",
43 | "npm": "6.14.10"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/seed/clear.ts:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | import { connectDatabase } from 'database';
3 |
4 | const clear = async () => {
5 | try {
6 | console.log('[clear] : running...');
7 | const db = await connectDatabase();
8 | const bookings = await db.bookings.find({}).toArray();
9 | const listings = await db.listings.find({}).toArray();
10 | const users = await db.users.find({}).toArray();
11 |
12 | if (bookings.length > 0) {
13 | await db.bookings.drop();
14 | }
15 |
16 | if (listings.length > 0) {
17 | await db.listings.drop();
18 | }
19 |
20 | if (users.length > 0) {
21 | await db.users.drop();
22 | }
23 |
24 | console.log('[clear] : success');
25 | } catch {
26 | throw new Error('[APP]: Failed to clear database');
27 | } finally {
28 | process.exit();
29 | }
30 | };
31 |
32 | clear();
33 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/database/index.ts:
--------------------------------------------------------------------------------
1 | import { IBooking, IDatabase, IListing, IUser } from 'lib/types';
2 | import { MongoClient } from 'mongodb';
3 |
4 | // const url = `mongodb+srv://${process.env.DB_USER}:${
5 | // process.env.DB_USER_PASSWORD
6 | // }@${process.env.DB_CLUSTER}.mongodb.net`;
7 |
8 | const url = `${process.env.MONGO_URI}`;
9 |
10 | export const connectDatabase = async (): Promise => {
11 | const client = await MongoClient.connect(url, {
12 | useNewUrlParser: true,
13 | useUnifiedTopology: true,
14 | });
15 | const db = client.db('main');
16 |
17 | return {
18 | bookings: db.collection('bookings'),
19 | listings: db.collection('listings'),
20 | users: db.collection('users'),
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/graphql/index.ts:
--------------------------------------------------------------------------------
1 | export * from './resolvers';
2 | export * from './typeDefs';
3 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/graphql/resolvers/Booking/types.ts:
--------------------------------------------------------------------------------
1 | export interface ICreateBookingInput {
2 | id: string;
3 | source: string;
4 | checkIn: string;
5 | checkOut: string;
6 | }
7 |
8 | export interface ICreateBookingArgs {
9 | input: ICreateBookingInput;
10 | }
11 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/graphql/resolvers/Listing/types.ts:
--------------------------------------------------------------------------------
1 | import { EListingType, IBooking, IListing } from 'lib/types';
2 |
3 | export enum EListingsFilter {
4 | PRICE_LOW_TO_HIGH = 'PRICE_LOW_TO_HIGH',
5 | PRICE_HIGH_TO_LOW = 'PRICE_HIGH_TO_LOW',
6 | }
7 |
8 | export interface IListingArgs {
9 | id: string;
10 | }
11 |
12 | export interface IListingBookingsArgs {
13 | limit: number;
14 | page: number;
15 | }
16 |
17 | export interface IListingBookingsData {
18 | total: number;
19 | result: IBooking[];
20 | }
21 |
22 | export interface IListingsArgs {
23 | location: string | null;
24 | filter: EListingsFilter;
25 | limit: number;
26 | page: number;
27 | }
28 |
29 | export interface IListingsData {
30 | region: string | null;
31 | total: number;
32 | result: IListing[];
33 | }
34 |
35 | export interface IListingsQuery {
36 | country?: string;
37 | admin?: string;
38 | city?: string;
39 | }
40 |
41 | export interface IHostListingInput {
42 | title: string;
43 | description: string;
44 | image: string;
45 | type: EListingType;
46 | address: string;
47 | price: number;
48 | numOfGuests: number;
49 | }
50 |
51 | export interface IHostListingArgs {
52 | input: IHostListingInput;
53 | }
54 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/graphql/resolvers/User/index.ts:
--------------------------------------------------------------------------------
1 | import { IResolvers } from 'apollo-server-express';
2 | import { Request } from 'express';
3 | import { IDatabase, IUser } from 'lib/types';
4 | import { authorize } from 'lib/utils';
5 | import {
6 | IUserArgs,
7 | IUserBookingsArgs,
8 | IUserBookingsData,
9 | IUserListingsArgs,
10 | IUserListingsData,
11 | } from './types';
12 |
13 | export const userResolvers: IResolvers = {
14 | Query: {
15 | user: async (
16 | _root: undefined,
17 | { id }: IUserArgs,
18 | { db, req }: { db: IDatabase; req: Request }
19 | ): Promise => {
20 | try {
21 | const user = await db.users.findOne({ _id: id });
22 |
23 | if (!user) {
24 | throw new Error("[APP]: User can't be found");
25 | }
26 |
27 | const viewer = await authorize(db, req);
28 |
29 | if (viewer && viewer._id === user._id) {
30 | user.authorized = true;
31 | }
32 | return user;
33 | } catch (error) {
34 | throw new Error(`[APP]: Failed to query user: ${error}`);
35 | }
36 | },
37 | },
38 | User: {
39 | id: (user: IUser): string => {
40 | return user._id;
41 | },
42 | hasWallet: (user: IUser): boolean => {
43 | return Boolean(user.walletId);
44 | },
45 | income: (user: IUser): number | null => {
46 | return user.authorized ? user.income : null;
47 | },
48 | bookings: async (
49 | user: IUser,
50 | { limit, page }: IUserBookingsArgs,
51 | { db }: { db: IDatabase }
52 | ): Promise => {
53 | try {
54 | if (!user.authorized) {
55 | return null;
56 | }
57 |
58 | const data: IUserBookingsData = {
59 | total: 0,
60 | result: [],
61 | };
62 |
63 | let cursor = await db.bookings.find({
64 | _id: { $in: user.bookings },
65 | });
66 |
67 | cursor = cursor.skip(page > 0 ? (page - 1) * limit : 0);
68 | cursor = cursor.limit(limit);
69 |
70 | data.total = await cursor.count();
71 | data.result = await cursor.toArray();
72 |
73 | return data;
74 | } catch (error) {
75 | throw new Error(`[APP]: Failed to query user bookings: ${error}`);
76 | }
77 | },
78 | listings: async (
79 | user: IUser,
80 | { limit, page }: IUserListingsArgs,
81 | { db }: { db: IDatabase }
82 | ): Promise => {
83 | try {
84 | const data: IUserListingsData = {
85 | total: 0,
86 | result: [],
87 | };
88 |
89 | let cursor = await db.listings.find({
90 | _id: { $in: user.listings },
91 | });
92 |
93 | cursor = cursor.skip(page > 0 ? (page - 1) * limit : 0);
94 | cursor = cursor.limit(limit);
95 |
96 | data.total = await cursor.count();
97 | data.result = await cursor.toArray();
98 |
99 | return data;
100 | } catch (error) {
101 | throw new Error(`[APP]: Failed to query user listings: ${error}`);
102 | }
103 | },
104 | },
105 | };
106 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/graphql/resolvers/User/types.ts:
--------------------------------------------------------------------------------
1 | import { IBooking, IListing } from 'lib/types';
2 |
3 | export interface IUserArgs {
4 | id: string;
5 | }
6 |
7 | export interface IUserBookingsArgs {
8 | limit: number;
9 | page: number;
10 | }
11 |
12 | export interface IUserBookingsData {
13 | total: number;
14 | result: IBooking[];
15 | }
16 |
17 | export interface IUserListingsArgs {
18 | limit: number;
19 | page: number;
20 | }
21 |
22 | export interface IUserListingsData {
23 | total: number;
24 | result: IListing[];
25 | }
26 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/graphql/resolvers/Viewer/types.ts:
--------------------------------------------------------------------------------
1 | export interface ILogInArgs {
2 | input: { code: string } | null;
3 | }
4 |
5 | export interface IConnectStripeArgs {
6 | input: { code: string };
7 | }
8 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/graphql/resolvers/index.ts:
--------------------------------------------------------------------------------
1 | import merge from 'lodash.merge';
2 | import { userResolvers } from './User';
3 | import { listingResolvers } from './Listing';
4 | import { bookingResolvers } from './Booking';
5 | import { viewerResolvers } from './Viewer';
6 |
7 | export const resolvers = merge(
8 | userResolvers,
9 | listingResolvers,
10 | bookingResolvers,
11 | viewerResolvers
12 | );
13 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/graphql/typeDefs.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server-express';
2 |
3 | export const typeDefs = gql`
4 | type Booking {
5 | id: ID!
6 | listing: Listing!
7 | tenant: User!
8 | checkIn: String!
9 | checkOut: String!
10 | }
11 |
12 | type Bookings {
13 | total: Int!
14 | result: [Booking!]!
15 | }
16 |
17 | enum ListingType {
18 | APARTMENT
19 | HOUSE
20 | }
21 |
22 | enum ListingsFilter {
23 | PRICE_LOW_TO_HIGH
24 | PRICE_HIGH_TO_LOW
25 | }
26 |
27 | type Listing {
28 | id: ID!
29 | title: String!
30 | description: String!
31 | image: String!
32 | host: User!
33 | type: ListingType!
34 | address: String!
35 | country: String!
36 | admin: String!
37 | city: String!
38 | bookings(limit: Int!, page: Int!): Bookings
39 | bookingsIndex: String!
40 | price: Int!
41 | numOfGuests: Int!
42 | }
43 |
44 | type Listings {
45 | region: String
46 | total: Int!
47 | result: [Listing!]!
48 | }
49 |
50 | type User {
51 | id: ID!
52 | name: String!
53 | avatar: String!
54 | contact: String!
55 | hasWallet: Boolean!
56 | income: Int
57 | bookings(limit: Int!, page: Int!): Bookings
58 | listings(limit: Int!, page: Int!): Listings!
59 | }
60 |
61 | type Viewer {
62 | id: ID
63 | token: String
64 | avatar: String
65 | hasWallet: Boolean
66 | didRequest: Boolean!
67 | }
68 |
69 | input LogInInput {
70 | code: String!
71 | }
72 |
73 | input ConnectStripeInput {
74 | code: String!
75 | }
76 |
77 | input HostListingInput {
78 | title: String!
79 | description: String!
80 | image: String!
81 | type: ListingType!
82 | address: String!
83 | price: Int!
84 | numOfGuests: Int!
85 | }
86 |
87 | input CreateBookingInput {
88 | id: ID!
89 | source: String!
90 | checkIn: String!
91 | checkOut: String!
92 | }
93 |
94 | type Query {
95 | authUrl: String!
96 | user(id: ID!): User!
97 | listing(id: ID!): Listing!
98 | listings(
99 | location: String
100 | filter: ListingsFilter!
101 | limit: Int!
102 | page: Int!
103 | ): Listings!
104 | }
105 |
106 | type Mutation {
107 | logIn(input: LogInInput): Viewer!
108 | logOut: Viewer!
109 | connectStripe(input: ConnectStripeInput!): Viewer!
110 | disconnectStripe: Viewer!
111 | hostListing(input: HostListingInput!): Listing!
112 | createBooking(input: CreateBookingInput!): Booking!
113 | }
114 | `;
115 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/index.ts:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | import { ApolloServer } from 'apollo-server-express';
4 | // import bodyParser from 'body-parser';
5 | import cookieParser from 'cookie-parser';
6 | import { connectDatabase } from 'database';
7 | import express, { Application } from 'express';
8 | import { resolvers, typeDefs } from './graphql';
9 |
10 | // const allowUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0';
11 | // console.log('ALLOW UNAUTHORIZED', allowUnauthorized);
12 |
13 | const envChecks = () => {
14 | if (!process.env.GRAPHQL_PORT) {
15 | throw new Error('[APP]: GRAPHQL_PORT must be defined');
16 | }
17 |
18 | if (!process.env.MONGO_URI) {
19 | throw new Error('[APP]: MONGO_URI must be defined');
20 | }
21 |
22 | if (!process.env.PUBLIC_URL) {
23 | throw new Error('[APP]: PUBLIC_URL must be defined');
24 | }
25 |
26 | if (!process.env.GOOGLE_CLIENT_ID) {
27 | throw new Error('[APP]: GOOGLE_CLIENT_ID must be defined');
28 | }
29 |
30 | if (!process.env.GOOGLE_CLIENT_SECRET) {
31 | throw new Error('[APP]: GOOGLE_CLIENT_SECRET must be defined');
32 | }
33 |
34 | if (!process.env.COOKIE_PARSER_SECRET) {
35 | throw new Error('[APP]: COOKIE_PARSER_SECRET must be defined');
36 | }
37 |
38 | if (!process.env.NODE_ENV) {
39 | throw new Error('[APP]: NODE_ENV must be defined');
40 | }
41 |
42 | if (!process.env.GOOGLE_GEOCODING_API_KEY) {
43 | throw new Error('[APP]: GOOGLE_GEOCODING_API_KEY must be defined');
44 | }
45 | };
46 |
47 | envChecks();
48 |
49 | const mount = async (app: Application) => {
50 | const db = await connectDatabase();
51 |
52 | // app.use(
53 | // bodyParser.json({
54 | // limit: '2mb',
55 | // })
56 | // );
57 |
58 | app.use(cookieParser(process.env.COOKIE_PARSER_SECRET));
59 |
60 | const server = new ApolloServer({
61 | typeDefs,
62 | resolvers,
63 | context: ({ req, res }) => ({ db, req, res }),
64 | });
65 |
66 | server.applyMiddleware({ app, path: '/api' });
67 | app.listen(process.env.GRAPHQL_PORT, () => {
68 | console.log(`[app] : http://localhost:${process.env.GRAPHQL_PORT}/api/`);
69 | });
70 | };
71 |
72 | mount(express());
73 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/lib/api/Cloudinary.ts:
--------------------------------------------------------------------------------
1 | import cloudinary from 'cloudinary';
2 |
3 | export const Cloudinary = {
4 | upload: async (image: string) => {
5 | const res = await cloudinary.v2.uploader.upload(image, {
6 | cloud_name: process.env.CLOUDINARY_NAME,
7 | api_key: process.env.CLOUDINARY_KEY,
8 | api_secret: process.env.CLOUDINARY_SECRET,
9 | folder: 'TH_Assets/',
10 | });
11 |
12 | return res.secure_url;
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/lib/api/Google.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AddressComponent,
3 | AddressType,
4 | Client,
5 | GeocodingAddressComponentType,
6 | } from '@googlemaps/google-maps-services-js';
7 | import { google } from 'googleapis';
8 |
9 | const auth = new google.auth.OAuth2(
10 | process.env.GOOGLE_CLIENT_ID,
11 | process.env.GOOGLE_CLIENT_SECRET,
12 | `${process.env.PUBLIC_URL}/login`
13 | );
14 |
15 | const maps = new Client({});
16 |
17 | const parseAddress = (addressComponents: AddressComponent[]) => {
18 | let country = null;
19 | let admin = null;
20 | let city = null;
21 |
22 | for (const component of addressComponents) {
23 | if (component.types.includes(AddressType.country)) {
24 | country = component.long_name;
25 | }
26 |
27 | if (component.types.includes(AddressType.administrative_area_level_1)) {
28 | admin = component.long_name;
29 | }
30 |
31 | if (
32 | component.types.includes(AddressType.locality) ||
33 | component.types.includes(GeocodingAddressComponentType.postal_town)
34 | ) {
35 | city = component.long_name;
36 | }
37 | }
38 |
39 | return { country, admin, city };
40 | };
41 |
42 | export const Google = {
43 | authUrl: auth.generateAuthUrl({
44 | access_type: 'online',
45 | scope: [
46 | 'https://www.googleapis.com/auth/userinfo.email',
47 | 'https://www.googleapis.com/auth/userinfo.profile',
48 | ],
49 | }),
50 | logIn: async (code: string) => {
51 | const { tokens } = await auth.getToken(code);
52 |
53 | auth.setCredentials(tokens);
54 |
55 | const { data } = await google.people({ version: 'v1', auth }).people.get({
56 | resourceName: 'people/me',
57 | personFields: 'emailAddresses,names,photos',
58 | });
59 |
60 | return { user: data };
61 | },
62 | geocode: async (address: string) => {
63 | if (!process.env.GOOGLE_GEOCODING_API_KEY)
64 | throw new Error('[App] Missing Google Maps API key');
65 |
66 | const res = await maps.geocode({
67 | params: {
68 | address,
69 | key: process.env.GOOGLE_GEOCODING_API_KEY!,
70 | },
71 | });
72 |
73 | if (res.status < 200 || res.status > 299) {
74 | throw new Error('[App] Failed to geocode address!');
75 | }
76 |
77 | return parseAddress(res.data.results[0].address_components);
78 | },
79 | };
80 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/lib/api/Stripe.ts:
--------------------------------------------------------------------------------
1 | import stripe from 'stripe';
2 |
3 | const client = new stripe(`${process.env.STRIPE_SECRET_KEY}`, {
4 | apiVersion: '2020-08-27',
5 | });
6 |
7 | export const Stripe = {
8 | connect: async (code: string) => {
9 | const response = await client.oauth.token({
10 | // @ts-ignore
11 | grand_type: 'authorization_code',
12 | code,
13 | });
14 |
15 | // if (!response) {
16 | // return new Error('[App] failed to connect with Stripe');
17 | // }
18 |
19 | return response;
20 | },
21 | disconnect: async (stripeUserId: string) => {
22 | const response = await client.oauth.deauthorize({
23 | client_id: `${process.env.STRIPE_CONNECT_CLIENT_ID}`,
24 | stripe_user_id: stripeUserId,
25 | });
26 |
27 | return response;
28 | },
29 | charge: async (amount: number, source: string, stripeAccount: string) => {
30 | /* eslint-disable @typescript-eslint/camelcase */
31 | const res = await client.charges.create(
32 | {
33 | amount,
34 | currency: 'usd',
35 | source,
36 | application_fee_amount: Math.round(amount * 0.05),
37 | },
38 | {
39 | stripe_account: stripeAccount,
40 | }
41 | );
42 | /* eslint-enable @typescript-eslint/camelcase */
43 |
44 | if (res.status !== 'succeeded') {
45 | throw new Error('[App] failed to create with Stripe');
46 | }
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/lib/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Cloudinary';
2 | export * from './Google';
3 | export * from './Stripe';
4 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { Collection, ObjectId } from 'mongodb';
2 |
3 | export interface IViewer {
4 | _id?: string;
5 | token?: string;
6 | avatar?: string;
7 | walletId?: string;
8 | didRequest: boolean;
9 | }
10 |
11 | export enum EListingType {
12 | Apartment = 'APARTMENT',
13 | House = 'HOUSE',
14 | }
15 |
16 | export interface IBookingIndexMonth {
17 | [key: string]: boolean;
18 | }
19 |
20 | export interface IBookingsIndexYear {
21 | [key: string]: IBookingIndexMonth;
22 | }
23 |
24 | export interface IBookingsIndex {
25 | [key: string]: IBookingsIndexYear;
26 | }
27 |
28 | export interface IBooking {
29 | _id: ObjectId;
30 | listing: ObjectId;
31 | tenant: string;
32 | checkIn: string;
33 | checkOut: string;
34 | }
35 |
36 | export interface IListing {
37 | _id: ObjectId;
38 | title: string;
39 | description: string;
40 | image: string;
41 | host: string;
42 | type: EListingType;
43 | address: string;
44 | country: string;
45 | admin: string;
46 | city: string;
47 | bookings: ObjectId[];
48 | bookingsIndex: IBookingsIndex;
49 | price: number;
50 | numOfGuests: number;
51 | authorized?: boolean;
52 | }
53 |
54 | export interface IUser {
55 | _id: string;
56 | token: string;
57 | name: string;
58 | avatar: string;
59 | contact: string;
60 | walletId?: string;
61 | income: number;
62 | bookings: ObjectId[];
63 | listings: ObjectId[];
64 | authorized?: boolean;
65 | }
66 |
67 | export interface IDatabase {
68 | bookings: Collection;
69 | listings: Collection;
70 | users: Collection;
71 | }
72 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/src/lib/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { Request } from 'express';
2 | import { IDatabase, IUser } from 'lib/types';
3 |
4 | export const authorize = async (
5 | db: IDatabase,
6 | req: Request
7 | ): Promise => {
8 | const token = req.get('X-CSRF-TOKEN');
9 |
10 | const viewer = await db.users.findOne({
11 | _id: req.signedCookies.viewer,
12 | token,
13 | });
14 |
15 | return viewer;
16 | };
17 |
--------------------------------------------------------------------------------
/app2/app/server/mondodb/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "target": "ES2020",
5 | "module": "commonjs",
6 | "rootDir": "./src",
7 | "outDir": "./build",
8 | "esModuleInterop": true,
9 | "strict": true
10 | },
11 | "exclude": ["temp"]
12 | }
13 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/.dockerignore:
--------------------------------------------------------------------------------
1 | **/.eslintignore
2 | **/.eslintrc
3 | **/.prettierignore
4 | **/.prettierrc
5 |
6 | **/node_modules/
7 | **/node_modules_linux/
8 | **/Dockerfile
9 | **/.dockerignore
10 | **/.gitignore
11 | **/.git
12 | **/README.md
13 | **/Readme.md
14 | **/LICENSE
15 | **/.vscode
16 |
17 | **/test/
18 | **/.next/
--------------------------------------------------------------------------------
/app2/app/server/postgresql/.env:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
2 | COOKIE_PARSER_SECRET=this-is-a-secret
3 | NODE_TLS_REJECT_UNAUTHORIZED=0
4 |
5 | CLOUDINARY_NAME=webmakaka
6 | CLOUDINARY_KEY=738764358432137
7 | CLOUDINARY_SECRET=uMdEieOVTmtNhDzwJpN3Dq46IiE
8 |
9 | GOOGLE_CLIENT_ID=FAKE_GOOGLE_CLIENT_ID
10 | GOOGLE_CLIENT_SECRET=FAKE_GOOGLE_CLIENT_SECRET
11 | GOOGLE_GEOCODING_API_KEY=FAKE_GOOGLE_GEOCODING_API_KEY
12 |
13 | PUBLIC_URL=https://tinyhouse.dev
14 | GRAPHQL_PORT=3000
15 |
16 | DATABASE_HOST=localhost
17 | DATABASE_NAME=postgres
18 | DATABASE_PORT=5432
19 | DATABASE_USER=postgres
20 | DATABASE_PASSWORD=pass123
21 | SECRET_KEY=1HyftmH8tPzutU46s2MXM87QuF844WKm
--------------------------------------------------------------------------------
/app2/app/server/postgresql/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": { "node": true },
3 | "plugins": ["@typescript-eslint"],
4 | "parser": "@typescript-eslint/parser",
5 | "parserOptions": {
6 | "ecmaVersion": 2020,
7 | "project": "tsconfig.json"
8 | },
9 | "extends": [
10 | "eslint:recommended",
11 | "plugin:@typescript-eslint/recommended",
12 | "plugin:@typescript-eslint/recommended-requiring-type-checking"
13 | ],
14 | "rules": {
15 | "prefer-const": "error",
16 | "@typscript-eslint/no-unused-vars": "off",
17 | "@typscript-eslint/no-unused-params": "off"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules/
3 |
4 | # production
5 | build/
6 |
7 | # misc
8 | .DS_Store
9 |
10 | # environment variables
11 | #.env
12 |
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
--------------------------------------------------------------------------------
/app2/app/server/postgresql/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine3.12
2 |
3 | WORKDIR /app
4 | COPY package.json ./
5 | # RUN npm install --only=prod --silent
6 |
7 | RUN npm install --silent
8 | COPY ./ ./
9 | CMD ["npm", "run", "start"]
--------------------------------------------------------------------------------
/app2/app/server/postgresql/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | mongodb-dev:
4 | image: mongo
5 | restart: always
6 | ports:
7 | - '27017:27017'
8 | environment:
9 | MONGODB_DATABASE: mongo-database
10 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/ormconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "postgres",
3 | "host": "localhost",
4 | "port": 5432,
5 | "username": "postgres",
6 | "password": "pass123",
7 | "database": "postgres",
8 | "synchronize": true,
9 | "logging": false,
10 | "entities": ["src/database/entity/**/*.ts"],
11 | "migrations": ["src/database/migration/**/*.ts"],
12 | "subscribers": ["src/database/subscriber/**/*.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tinyhouse-server",
3 | "version": "0.1.0",
4 | "dependencies": {
5 | "@googlemaps/google-maps-services-js": "^3.1.16",
6 | "apollo-server-express": "^2.25.1",
7 | "body-parser": "^1.19.0",
8 | "cloudinary": "^1.26.0",
9 | "cookie-parser": "^1.4.5",
10 | "express": "^4.17.1",
11 | "googleapis": "^75.0.0",
12 | "graphql": "^15.5.0",
13 | "lodash.merge": "^4.6.2",
14 | "pg": "^8.6.0",
15 | "reflect-metadata": "^0.1.13",
16 | "stripe": "^8.154.0",
17 | "typeorm": "^0.2.34"
18 | },
19 | "devDependencies": {
20 | "@types/body-parser": "^1.19.0",
21 | "@types/cookie-parser": "^1.4.2",
22 | "@types/express": "^4.17.12",
23 | "@types/graphql": "^14.5.0",
24 | "@types/lodash.merge": "^4.6.6",
25 | "@types/node": "^15.12.2",
26 | "@types/stripe": "^8.0.417",
27 | "@typescript-eslint/eslint-plugin": "^4.26.1",
28 | "@typescript-eslint/parser": "^4.26.1",
29 | "dotenv": "^10.0.0",
30 | "eslint": "^7.28.0",
31 | "nodemon": "^2.0.7",
32 | "ts-node": "^10.0.0",
33 | "typescript": "^4.3.2"
34 | },
35 | "scripts": {
36 | "start": "NODE_PATH=./src nodemon src/index.ts",
37 | "seed": "NODE_PATH=./src ts-node seed/seed.ts",
38 | "clear": "NODE_PATH=./src ts-node seed/clear.ts",
39 | "build": "NODE_PATH=./src tsc -p ./",
40 | "lint": "NODE_PATH=./src eslint '**/*.ts'"
41 | },
42 | "volta": {
43 | "node": "14.15.3",
44 | "npm": "6.14.10"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/seed/clear.ts:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | import { connectDatabase } from 'database';
3 |
4 | const clear = async () => {
5 | try {
6 | console.log('[clear] : running...');
7 | const db = await connectDatabase();
8 |
9 | await db.bookings.clear();
10 | await db.listings.clear();
11 | await db.users.clear();
12 |
13 | console.log('[clear] : success');
14 | } catch {
15 | throw new Error('[APP]: Failed to clear database');
16 | } finally {
17 | process.exit();
18 | }
19 | };
20 |
21 | clear();
22 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/database/entity/BookingEntity.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm';
2 |
3 | @Entity('bookings')
4 | export class BookingEntity extends BaseEntity {
5 | @PrimaryColumn('text')
6 | id: string;
7 |
8 | @Column('text')
9 | listing: string;
10 |
11 | @Column('text')
12 | tenant: string;
13 |
14 | @Column('text')
15 | checkIn: string;
16 |
17 | @Column('text')
18 | checkOut: string;
19 | }
20 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/database/entity/ListingEntity.ts:
--------------------------------------------------------------------------------
1 | import { EListingType, IBookingsIndex } from 'lib/types';
2 | import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm';
3 |
4 | @Entity('listings')
5 | export class ListingEntity extends BaseEntity {
6 | @PrimaryColumn('text')
7 | id: string;
8 |
9 | @Column('varchar', { length: 100 })
10 | title: string;
11 |
12 | @Column('varchar', { length: 5000 })
13 | description: string;
14 |
15 | @Column('text')
16 | image: string;
17 |
18 | @Column('text')
19 | host: string;
20 |
21 | @Column({ type: 'enum', enum: EListingType })
22 | type: EListingType;
23 |
24 | @Column('text')
25 | address: string;
26 |
27 | @Column('text')
28 | country: string;
29 |
30 | @Column('text')
31 | admin: string;
32 |
33 | @Column('text')
34 | city: string;
35 |
36 | @Column('simple-array')
37 | bookings: string[];
38 |
39 | @Column('simple-json')
40 | bookingsIndex: IBookingsIndex;
41 |
42 | @Column('integer')
43 | price: number;
44 |
45 | @Column('integer')
46 | numOfGuests: number;
47 | }
48 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/database/entity/UserEntity.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm';
2 |
3 | @Entity('users')
4 | export class UserEntity extends BaseEntity {
5 | @PrimaryColumn('text')
6 | id: string;
7 |
8 | @Column('text')
9 | token: string;
10 |
11 | @Column('text')
12 | name: string;
13 |
14 | @Column('text')
15 | avatar: string;
16 |
17 | @Column('text')
18 | contact: string;
19 |
20 | @Column('text', { nullable: true })
21 | walletId?: string | null;
22 |
23 | @Column('integer')
24 | income: number;
25 |
26 | @Column('simple-array')
27 | bookings: string[];
28 |
29 | @Column('simple-array')
30 | listings: string[];
31 | }
32 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/database/entity/index.ts:
--------------------------------------------------------------------------------
1 | export * from './BookingEntity';
2 | export * from './ListingEntity';
3 | export * from './UserEntity';
4 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/database/index.ts:
--------------------------------------------------------------------------------
1 | import { IDatabase } from 'lib/types';
2 | import { createConnection } from 'typeorm';
3 | import { BookingEntity, ListingEntity, UserEntity } from './entity';
4 |
5 | export const connectDatabase = async (): Promise => {
6 | const connection = await createConnection();
7 |
8 | return {
9 | bookings: connection.getRepository(BookingEntity),
10 | listings: connection.getRepository(ListingEntity),
11 | users: connection.getRepository(UserEntity),
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/graphql/index.ts:
--------------------------------------------------------------------------------
1 | export * from './resolvers';
2 | export * from './typeDefs';
3 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/graphql/resolvers/Booking/types.ts:
--------------------------------------------------------------------------------
1 | export interface ICreateBookingInput {
2 | id: string;
3 | source: string;
4 | checkIn: string;
5 | checkOut: string;
6 | }
7 |
8 | export interface ICreateBookingArgs {
9 | input: ICreateBookingInput;
10 | }
11 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/graphql/resolvers/Listing/types.ts:
--------------------------------------------------------------------------------
1 | import { EListingType, IBooking, IListing } from 'lib/types';
2 |
3 | export enum EListingsFilter {
4 | PRICE_LOW_TO_HIGH = 'PRICE_LOW_TO_HIGH',
5 | PRICE_HIGH_TO_LOW = 'PRICE_HIGH_TO_LOW',
6 | }
7 |
8 | export interface IListingArgs {
9 | id: string;
10 | }
11 |
12 | export interface IListingBookingsArgs {
13 | limit: number;
14 | page: number;
15 | }
16 |
17 | export interface IListingBookingsData {
18 | total: number;
19 | result: IBooking[];
20 | }
21 |
22 | export interface IListingsArgs {
23 | location: string | null;
24 | filter: EListingsFilter;
25 | limit: number;
26 | page: number;
27 | }
28 |
29 | export interface IListingsData {
30 | region: string | null;
31 | total: number;
32 | result: IListing[];
33 | }
34 |
35 | export interface IListingsQuery {
36 | country?: string;
37 | admin?: string;
38 | city?: string;
39 | }
40 |
41 | export interface IHostListingInput {
42 | title: string;
43 | description: string;
44 | image: string;
45 | type: EListingType;
46 | address: string;
47 | price: number;
48 | numOfGuests: number;
49 | }
50 |
51 | export interface IHostListingArgs {
52 | input: IHostListingInput;
53 | }
54 |
55 | export interface IOrder {
56 | price: 1 | 'ASC' | 'DESC' | -1 | undefined;
57 | }
58 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/graphql/resolvers/User/index.ts:
--------------------------------------------------------------------------------
1 | import { IResolvers } from 'apollo-server-express';
2 | import { Request } from 'express';
3 | import { IDatabase, IUser } from 'lib/types';
4 | import { authorize } from 'lib/utils';
5 | import {
6 | IUserArgs,
7 | IUserBookingsArgs,
8 | IUserBookingsData,
9 | IUserListingsArgs,
10 | IUserListingsData,
11 | } from './types';
12 |
13 | export const userResolvers: IResolvers = {
14 | Query: {
15 | user: async (
16 | _root: undefined,
17 | { id }: IUserArgs,
18 | { db, req }: { db: IDatabase; req: Request }
19 | ): Promise => {
20 | try {
21 | const user = (await db.users.findOne({ id })) as IUser;
22 |
23 | if (!user) {
24 | throw new Error("[APP]: User can't be found");
25 | }
26 |
27 | const viewer = await authorize(db, req);
28 |
29 | if (viewer && viewer.id === user.id) {
30 | user.authorized = true;
31 | }
32 |
33 | return user;
34 | } catch (error) {
35 | throw new Error(`[APP]: Failed to query user: ${error}`);
36 | }
37 | },
38 | },
39 | User: {
40 | hasWallet: (user: IUser): boolean => {
41 | return Boolean(user.walletId);
42 | },
43 | income: (user: IUser): number | null => {
44 | return user.authorized ? user.income : null;
45 | },
46 | bookings: async (
47 | user: IUser,
48 | { limit, page }: IUserBookingsArgs,
49 | { db }: { db: IDatabase }
50 | ): Promise => {
51 | try {
52 | if (!user.authorized) {
53 | return null;
54 | }
55 |
56 | const data: IUserBookingsData = {
57 | total: 0,
58 | result: [],
59 | };
60 |
61 | const bookings = await db.bookings.findByIds(user.bookings, {
62 | skip: page > 0 ? (page - 1) * limit : 0,
63 | take: limit,
64 | });
65 |
66 | data.total = user.bookings.length;
67 | data.result = bookings;
68 |
69 | return data;
70 | } catch (error) {
71 | throw new Error(`[APP]: Failed to query user bookings: ${error}`);
72 | }
73 | },
74 | listings: async (
75 | user: IUser,
76 | { limit, page }: IUserListingsArgs,
77 | { db }: { db: IDatabase }
78 | ): Promise => {
79 | try {
80 | const data: IUserListingsData = {
81 | total: 0,
82 | result: [],
83 | };
84 |
85 | const listings = await db.listings.findByIds(user.listings, {
86 | skip: page > 0 ? (page - 1) * limit : 0,
87 | take: limit,
88 | });
89 |
90 | data.total = user.listings.length;
91 | data.result = listings;
92 |
93 | return data;
94 | } catch (error) {
95 | throw new Error(`[APP]: Failed to query user listings: ${error}`);
96 | }
97 | },
98 | },
99 | };
100 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/graphql/resolvers/User/types.ts:
--------------------------------------------------------------------------------
1 | import { IBooking, IListing } from 'lib/types';
2 |
3 | export interface IUserArgs {
4 | id: string;
5 | }
6 |
7 | export interface IUserBookingsArgs {
8 | limit: number;
9 | page: number;
10 | }
11 |
12 | export interface IUserBookingsData {
13 | total: number;
14 | result: IBooking[];
15 | }
16 |
17 | export interface IUserListingsArgs {
18 | limit: number;
19 | page: number;
20 | }
21 |
22 | export interface IUserListingsData {
23 | total: number;
24 | result: IListing[];
25 | }
26 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/graphql/resolvers/Viewer/types.ts:
--------------------------------------------------------------------------------
1 | export interface ILogInArgs {
2 | input: { code: string } | null;
3 | }
4 |
5 | export interface IConnectStripeArgs {
6 | input: { code: string };
7 | }
8 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/graphql/resolvers/index.ts:
--------------------------------------------------------------------------------
1 | import merge from 'lodash.merge';
2 | import { userResolvers } from './User';
3 | import { listingResolvers } from './Listing';
4 | import { bookingResolvers } from './Booking';
5 | import { viewerResolvers } from './Viewer';
6 |
7 | export const resolvers = merge(
8 | userResolvers,
9 | listingResolvers,
10 | bookingResolvers,
11 | viewerResolvers
12 | );
13 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/graphql/typeDefs.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server-express';
2 |
3 | export const typeDefs = gql`
4 | type Booking {
5 | id: ID!
6 | listing: Listing!
7 | tenant: User!
8 | checkIn: String!
9 | checkOut: String!
10 | }
11 |
12 | type Bookings {
13 | total: Int!
14 | result: [Booking!]!
15 | }
16 |
17 | enum ListingType {
18 | APARTMENT
19 | HOUSE
20 | }
21 |
22 | enum ListingsFilter {
23 | PRICE_LOW_TO_HIGH
24 | PRICE_HIGH_TO_LOW
25 | }
26 |
27 | type Listing {
28 | id: ID!
29 | title: String!
30 | description: String!
31 | image: String!
32 | host: User!
33 | type: ListingType!
34 | address: String!
35 | country: String!
36 | admin: String!
37 | city: String!
38 | bookings(limit: Int!, page: Int!): Bookings
39 | bookingsIndex: String!
40 | price: Int!
41 | numOfGuests: Int!
42 | }
43 |
44 | type Listings {
45 | region: String
46 | total: Int!
47 | result: [Listing!]!
48 | }
49 |
50 | type User {
51 | id: ID!
52 | name: String!
53 | avatar: String!
54 | contact: String!
55 | hasWallet: Boolean!
56 | income: Int
57 | bookings(limit: Int!, page: Int!): Bookings
58 | listings(limit: Int!, page: Int!): Listings!
59 | }
60 |
61 | type Viewer {
62 | id: ID
63 | token: String
64 | avatar: String
65 | hasWallet: Boolean
66 | didRequest: Boolean!
67 | }
68 |
69 | input LogInInput {
70 | code: String!
71 | }
72 |
73 | input ConnectStripeInput {
74 | code: String!
75 | }
76 |
77 | input HostListingInput {
78 | title: String!
79 | description: String!
80 | image: String!
81 | type: ListingType!
82 | address: String!
83 | price: Int!
84 | numOfGuests: Int!
85 | }
86 |
87 | input CreateBookingInput {
88 | id: ID!
89 | source: String!
90 | checkIn: String!
91 | checkOut: String!
92 | }
93 |
94 | type Query {
95 | authUrl: String!
96 | user(id: ID!): User!
97 | listing(id: ID!): Listing!
98 | listings(
99 | location: String
100 | filter: ListingsFilter!
101 | limit: Int!
102 | page: Int!
103 | ): Listings!
104 | }
105 |
106 | type Mutation {
107 | logIn(input: LogInInput): Viewer!
108 | logOut: Viewer!
109 | connectStripe(input: ConnectStripeInput!): Viewer!
110 | disconnectStripe: Viewer!
111 | hostListing(input: HostListingInput!): Listing!
112 | createBooking(input: CreateBookingInput!): Booking!
113 | }
114 | `;
115 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/index.ts:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | import { ApolloServer } from 'apollo-server-express';
4 | // import bodyParser from 'body-parser';
5 | import cookieParser from 'cookie-parser';
6 | import { connectDatabase } from 'database';
7 | import express, { Application } from 'express';
8 | import 'reflect-metadata';
9 | import { resolvers, typeDefs } from './graphql';
10 |
11 | // const allowUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0';
12 | // console.log('ALLOW UNAUTHORIZED', allowUnauthorized);
13 |
14 | const envChecks = () => {
15 | if (!process.env.GRAPHQL_PORT) {
16 | throw new Error('[APP]: GRAPHQL_PORT must be defined');
17 | }
18 |
19 | if (!process.env.PUBLIC_URL) {
20 | throw new Error('[APP]: PUBLIC_URL must be defined');
21 | }
22 |
23 | if (!process.env.GOOGLE_CLIENT_ID) {
24 | throw new Error('[APP]: GOOGLE_CLIENT_ID must be defined');
25 | }
26 |
27 | if (!process.env.GOOGLE_CLIENT_SECRET) {
28 | throw new Error('[APP]: GOOGLE_CLIENT_SECRET must be defined');
29 | }
30 |
31 | if (!process.env.COOKIE_PARSER_SECRET) {
32 | throw new Error('[APP]: COOKIE_PARSER_SECRET must be defined');
33 | }
34 |
35 | if (!process.env.NODE_ENV) {
36 | throw new Error('[APP]: NODE_ENV must be defined');
37 | }
38 |
39 | if (!process.env.GOOGLE_GEOCODING_API_KEY) {
40 | throw new Error('[APP]: GOOGLE_GEOCODING_API_KEY must be defined');
41 | }
42 | };
43 |
44 | envChecks();
45 |
46 | const mount = async (app: Application) => {
47 | const db = await connectDatabase();
48 |
49 | // app.use(
50 | // bodyParser.json({
51 | // limit: '2mb',
52 | // })
53 | // );
54 |
55 | app.use(cookieParser(process.env.COOKIE_PARSER_SECRET));
56 |
57 | const server = new ApolloServer({
58 | typeDefs,
59 | resolvers,
60 | context: ({ req, res }) => ({ db, req, res }),
61 | });
62 |
63 | server.applyMiddleware({ app, path: '/api' });
64 | app.listen(process.env.GRAPHQL_PORT, () => {
65 | console.log(`[app] : http://localhost:${process.env.GRAPHQL_PORT}/api/`);
66 | });
67 | };
68 |
69 | mount(express());
70 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/lib/api/Cloudinary.ts:
--------------------------------------------------------------------------------
1 | import cloudinary from 'cloudinary';
2 |
3 | export const Cloudinary = {
4 | upload: async (image: string) => {
5 | const res = await cloudinary.v2.uploader.upload(image, {
6 | cloud_name: process.env.CLOUDINARY_NAME,
7 | api_key: process.env.CLOUDINARY_KEY,
8 | api_secret: process.env.CLOUDINARY_SECRET,
9 | folder: 'TH_Assets/',
10 | });
11 |
12 | return res.secure_url;
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/lib/api/Google.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AddressComponent,
3 | AddressType,
4 | Client,
5 | GeocodingAddressComponentType,
6 | } from '@googlemaps/google-maps-services-js';
7 | import { google } from 'googleapis';
8 |
9 | const auth = new google.auth.OAuth2(
10 | process.env.GOOGLE_CLIENT_ID,
11 | process.env.GOOGLE_CLIENT_SECRET,
12 | `${process.env.PUBLIC_URL}/login`
13 | );
14 |
15 | const maps = new Client({});
16 |
17 | const parseAddress = (addressComponents: AddressComponent[]) => {
18 | let country = null;
19 | let admin = null;
20 | let city = null;
21 |
22 | for (const component of addressComponents) {
23 | if (component.types.includes(AddressType.country)) {
24 | country = component.long_name;
25 | }
26 |
27 | if (component.types.includes(AddressType.administrative_area_level_1)) {
28 | admin = component.long_name;
29 | }
30 |
31 | if (
32 | component.types.includes(AddressType.locality) ||
33 | component.types.includes(GeocodingAddressComponentType.postal_town)
34 | ) {
35 | city = component.long_name;
36 | }
37 | }
38 |
39 | return { country, admin, city };
40 | };
41 |
42 | export const Google = {
43 | authUrl: auth.generateAuthUrl({
44 | access_type: 'online',
45 | scope: [
46 | 'https://www.googleapis.com/auth/userinfo.email',
47 | 'https://www.googleapis.com/auth/userinfo.profile',
48 | ],
49 | }),
50 | logIn: async (code: string) => {
51 | const { tokens } = await auth.getToken(code);
52 |
53 | auth.setCredentials(tokens);
54 |
55 | const { data } = await google.people({ version: 'v1', auth }).people.get({
56 | resourceName: 'people/me',
57 | personFields: 'emailAddresses,names,photos',
58 | });
59 |
60 | return { user: data };
61 | },
62 | geocode: async (address: string) => {
63 | if (!process.env.GOOGLE_GEOCODING_API_KEY)
64 | throw new Error('[App] Missing Google Maps API key');
65 |
66 | const res = await maps.geocode({
67 | params: {
68 | address,
69 | key: process.env.GOOGLE_GEOCODING_API_KEY!,
70 | },
71 | });
72 |
73 | if (res.status < 200 || res.status > 299) {
74 | throw new Error('[App] Failed to geocode address!');
75 | }
76 |
77 | return parseAddress(res.data.results[0].address_components);
78 | },
79 | };
80 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/lib/api/Stripe.ts:
--------------------------------------------------------------------------------
1 | import stripe from 'stripe';
2 |
3 | const client = new stripe(`${process.env.STRIPE_SECRET_KEY}`, {
4 | apiVersion: '2020-08-27',
5 | });
6 |
7 | export const Stripe = {
8 | connect: async (code: string) => {
9 | const response = await client.oauth.token({
10 | // @ts-ignore
11 | grand_type: 'authorization_code',
12 | code,
13 | });
14 |
15 | // if (!response) {
16 | // return new Error('[App] failed to connect with Stripe');
17 | // }
18 |
19 | return response;
20 | },
21 | disconnect: async (stripeUserId: string) => {
22 | const response = await client.oauth.deauthorize({
23 | client_id: `${process.env.STRIPE_CONNECT_CLIENT_ID}`,
24 | stripe_user_id: stripeUserId,
25 | });
26 |
27 | return response;
28 | },
29 | charge: async (amount: number, source: string, stripeAccount: string) => {
30 | /* eslint-disable @typescript-eslint/camelcase */
31 | const res = await client.charges.create(
32 | {
33 | amount,
34 | currency: 'usd',
35 | source,
36 | application_fee_amount: Math.round(amount * 0.05),
37 | },
38 | {
39 | stripe_account: stripeAccount,
40 | }
41 | );
42 | /* eslint-enable @typescript-eslint/camelcase */
43 |
44 | if (res.status !== 'succeeded') {
45 | throw new Error('[App] failed to create with Stripe');
46 | }
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/lib/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Cloudinary';
2 | export * from './Google';
3 | export * from './Stripe';
4 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { BookingEntity, ListingEntity, UserEntity } from 'database/entity';
2 | import { Repository } from 'typeorm';
3 |
4 | export interface IViewer {
5 | id?: string;
6 | token?: string;
7 | avatar?: string;
8 | walletId?: string | null;
9 | didRequest: boolean;
10 | }
11 |
12 | export enum EListingType {
13 | Apartment = 'APARTMENT',
14 | House = 'HOUSE',
15 | }
16 |
17 | export interface IBookingIndexMonth {
18 | [key: string]: boolean;
19 | }
20 |
21 | export interface IBookingsIndexYear {
22 | [key: string]: IBookingIndexMonth;
23 | }
24 |
25 | export interface IBookingsIndex {
26 | [key: string]: IBookingsIndexYear;
27 | }
28 |
29 | export interface IBooking {
30 | id: string;
31 | listing: string;
32 | tenant: string;
33 | checkIn: string;
34 | checkOut: string;
35 | }
36 |
37 | export interface IListing {
38 | id: string;
39 | title: string;
40 | description: string;
41 | image: string;
42 | host: string;
43 | type: EListingType;
44 | address: string;
45 | country: string;
46 | admin: string;
47 | city: string;
48 | bookings: string[];
49 | bookingsIndex: IBookingsIndex;
50 | price: number;
51 | numOfGuests: number;
52 | authorized?: boolean;
53 | }
54 |
55 | export interface IUser {
56 | id: string;
57 | token: string;
58 | name: string;
59 | avatar: string;
60 | contact: string;
61 | walletId?: string | null;
62 | income: number;
63 | bookings: string[];
64 | listings: string[];
65 | authorized?: boolean;
66 | }
67 |
68 | export interface IDatabase {
69 | bookings: Repository;
70 | listings: Repository;
71 | users: Repository;
72 | }
73 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/src/lib/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { UserEntity } from 'database/entity';
2 | import { Request } from 'express';
3 | import { IDatabase } from 'lib/types';
4 |
5 | export const authorize = async (
6 | db: IDatabase,
7 | req: Request
8 | ): Promise => {
9 | const token = req.get('X-CSRF-TOKEN');
10 |
11 | const viewer = await db.users.findOne({
12 | id: req.signedCookies.viewer,
13 | token,
14 | });
15 |
16 | if (!viewer) return null;
17 |
18 | return viewer;
19 | };
20 |
--------------------------------------------------------------------------------
/app2/app/server/postgresql/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "target": "ES2020",
5 | "module": "commonjs",
6 | "rootDir": "./src",
7 | "outDir": "./build",
8 | "esModuleInterop": true,
9 | "strict": true,
10 | "emitDecoratorMetadata": true,
11 | "experimentalDecorators": true,
12 | "strictPropertyInitialization": false
13 | },
14 | "exclude": ["temp"]
15 | }
16 |
--------------------------------------------------------------------------------
/app2/k8s/client-clusterIP.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: tinyhouse-client-svc
5 | spec:
6 | selector:
7 | app: tinyhouse-client
8 | ports:
9 | - name: tinyhouse-client
10 | protocol: TCP
11 | port: 3000
12 | targetPort: 3000
13 |
--------------------------------------------------------------------------------
/app2/k8s/client-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: tinyhouse-client-deployment
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: tinyhouse-client
10 | template:
11 | metadata:
12 | labels:
13 | app: tinyhouse-client
14 | spec:
15 | containers:
16 | - name: tinyhouse-client
17 | image: webmakaka/tinyhouse-client-app2
18 | env:
19 | - name: REACT_APP_STRIPE_PUBLISHABLE_KEY
20 | valueFrom:
21 | secretKeyRef:
22 | name: react-app-stripe-publishable-key
23 | key: REACT_APP_STRIPE_PUBLISHABLE_KEY
24 | - name: REACT_APP_STRIPE_CONNECT_CLIENT_ID
25 | valueFrom:
26 | secretKeyRef:
27 | name: react-app-stripe-connect-client-id
28 | key: REACT_APP_STRIPE_CONNECT_CLIENT_ID
29 |
--------------------------------------------------------------------------------
/app2/k8s/ingress-config.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: networking.k8s.io/v1
2 | kind: Ingress
3 | metadata:
4 | name: ingress-svc
5 | annotations:
6 | kubernetes.io/ingress.class: nginx
7 | nginx.ingress.kubernetes.io/use-regex: 'true'
8 | spec:
9 | rules:
10 | - host: tinyhouse.dev
11 | http:
12 | paths:
13 | - path: /api/?(.*)
14 | pathType: 'Prefix'
15 | backend:
16 | service:
17 | name: tinyhouse-server-svc
18 | port:
19 | number: 3000
20 | - path: /?(.*)
21 | pathType: 'Prefix'
22 | backend:
23 | service:
24 | name: tinyhouse-client-svc
25 | port:
26 | number: 3000
27 |
--------------------------------------------------------------------------------
/app2/k8s/mongo-clusterIP.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: tinyhouse-mongo-svc
5 | spec:
6 | selector:
7 | app: tinyhouse-mongo
8 | ports:
9 | - name: db
10 | protocol: TCP
11 | port: 27017
12 | targetPort: 27017
13 |
--------------------------------------------------------------------------------
/app2/k8s/mongo-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: tinyhouse-mongo-deployment
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: tinyhouse-mongo
10 | template:
11 | metadata:
12 | labels:
13 | app: tinyhouse-mongo
14 | spec:
15 | containers:
16 | - name: tinyhouse-mongo
17 | image: mongo:4.4.6-bionic
18 |
--------------------------------------------------------------------------------
/app2/k8s/server-clusterIP.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: tinyhouse-server-svc
5 | spec:
6 | selector:
7 | app: tinyhouse-server
8 | ports:
9 | - name: tinyhouse-server
10 | protocol: TCP
11 | port: 3000
12 | targetPort: 3000
13 |
--------------------------------------------------------------------------------
/app2/k8s/server-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: tinyhouse-server-deployment
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: tinyhouse-server
10 | template:
11 | metadata:
12 | labels:
13 | app: tinyhouse-server
14 | spec:
15 | containers:
16 | - name: tinyhouse-server
17 | image: webmakaka/tinyhouse-server-app2
18 | env:
19 | - name: PUBLIC_URL
20 | value: 'https://tinyhouse.dev'
21 | - name: GRAPHQL_PORT
22 | value: '3000'
23 | - name: MONGO_URI
24 | value: 'mongodb://tinyhouse-mongo-svc:27017'
25 | - name: GOOGLE_CLIENT_ID
26 | valueFrom:
27 | secretKeyRef:
28 | name: google-client-id
29 | key: GOOGLE_CLIENT_ID
30 | - name: GOOGLE_CLIENT_SECRET
31 | valueFrom:
32 | secretKeyRef:
33 | name: google-client-secret
34 | key: GOOGLE_CLIENT_SECRET
35 | - name: GOOGLE_GEOCODING_API_KEY
36 | valueFrom:
37 | secretKeyRef:
38 | name: google-geocoding-api-key
39 | key: GOOGLE_GEOCODING_API_KEY
40 | - name: STRIPE_SECRET_KEY
41 | valueFrom:
42 | secretKeyRef:
43 | name: stripe-secret-key
44 | key: STRIPE_SECRET_KEY
45 |
--------------------------------------------------------------------------------
/app2/skaffold/skaffold.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: skaffold/v2beta17
2 | kind: Config
3 | build:
4 | local:
5 | push: false
6 | tagPolicy:
7 | sha256: {}
8 | artifacts:
9 | - image: webmakaka/tinyhouse-client-app2
10 | context: ../app/client
11 | docker:
12 | dockerfile: Dockerfile.dev
13 | sync:
14 | manual:
15 | - src: 'src/**/*.ts*'
16 | dest: .
17 | - image: webmakaka/tinyhouse-server-app2
18 | context: ../app/server/mongodb
19 | docker:
20 | dockerfile: Dockerfile.dev
21 | sync:
22 | manual:
23 | - src: 'src/**/*.ts*'
24 | dest: .
25 | deploy:
26 | kubectl:
27 | manifests:
28 | - ../k8s/*
29 |
--------------------------------------------------------------------------------
/img/pic-app-1-final.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-app-1-final.png
--------------------------------------------------------------------------------
/img/pic-app-2-current.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-app-2-current.png
--------------------------------------------------------------------------------
/img/pic-m08-p01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m08-p01.png
--------------------------------------------------------------------------------
/img/pic-m08-p02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m08-p02.png
--------------------------------------------------------------------------------
/img/pic-m09-p01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m09-p01.png
--------------------------------------------------------------------------------
/img/pic-m09-p02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m09-p02.png
--------------------------------------------------------------------------------
/img/pic-m09-p03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m09-p03.png
--------------------------------------------------------------------------------
/img/pic-m09-p04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m09-p04.png
--------------------------------------------------------------------------------
/img/pic-m09-p05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m09-p05.png
--------------------------------------------------------------------------------
/img/pic-m09-p06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m09-p06.png
--------------------------------------------------------------------------------
/img/pic-m09-p07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m09-p07.png
--------------------------------------------------------------------------------
/img/pic-m09-p08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m09-p08.png
--------------------------------------------------------------------------------
/img/pic-m09-p09.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m09-p09.png
--------------------------------------------------------------------------------
/img/pic-m10-p01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m10-p01.png
--------------------------------------------------------------------------------
/img/pic-m10-p02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m10-p02.png
--------------------------------------------------------------------------------
/img/pic-m10-p03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m10-p03.png
--------------------------------------------------------------------------------
/img/pic-m10-p04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m10-p04.png
--------------------------------------------------------------------------------
/img/pic-m13-p01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-m13-p01.png
--------------------------------------------------------------------------------
/img/pic-setup-k9s-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-setup-k9s-01.png
--------------------------------------------------------------------------------
/img/pic-setup-k9s-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic-setup-k9s-02.png
--------------------------------------------------------------------------------
/img/pic19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic19.png
--------------------------------------------------------------------------------
/img/pic20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic20.png
--------------------------------------------------------------------------------
/img/pic21.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic21.png
--------------------------------------------------------------------------------
/img/pic22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic22.png
--------------------------------------------------------------------------------
/img/pic23.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic23.png
--------------------------------------------------------------------------------
/img/pic24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic24.png
--------------------------------------------------------------------------------
/img/pic35-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic35-1.png
--------------------------------------------------------------------------------
/img/pic35-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic35-2.png
--------------------------------------------------------------------------------
/img/pic36-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic36-1.png
--------------------------------------------------------------------------------
/img/pic42.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic42.png
--------------------------------------------------------------------------------
/img/pic43.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webmakaka/TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL/eb52fa82d3e8987f542a40496a391b08b946824e/img/pic43.png
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.organization=webmakaka
2 | sonar.projectKey=webmakaka_TinyHouse-A-Fullstack-React-Masterclass-with-TypeScript-and-GraphQL
3 | sonar.sources=.
4 |
--------------------------------------------------------------------------------