├── .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 | ![Application](/img/pic-setup-k9s-01.png?raw=true) 166 | 167 |
168 | 169 | ![Application](/img/pic-setup-k9s-02.png?raw=true) 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 | ![Application](/img/pic-app-1-final.png?raw=true) 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 | 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 | 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 | App logo 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 | <span>/day</span> 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 | 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 |
54 |
55 |
56 | 57 | App logo 58 | 59 |
60 |
61 | setSearch(evt.target.value)} 66 | onSearch={onSearch} 67 | /> 68 |
69 |
70 |
71 | 72 |
73 |
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 | San Fransisco 105 |
106 | 107 | 108 | 109 | 110 |
111 | Cancun 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 | 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 | <span role="img" aria-label="wave"> 97 | 💥 98 | </span> 99 | 100 | 101 | Log in to TinyHouse! 102 | 103 | 104 | Sign in with Google to start booking available rentals! 105 | 106 |
107 | 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 | 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 | 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 |
124 | 125 |
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 | --------------------------------------------------------------------------------