├── functions
├── .gitignore
├── package.json
└── src
│ └── index.js
├── go-api
├── .gitignore
├── go.mod
├── .gcloudignore
├── Dockerfile
├── database
│ └── firebase.go
├── logger
│ └── logger.go
├── main.go
├── model
│ └── hero.go
└── controllers
│ ├── hero.go
│ └── hero_test.go
├── dialogflow
├── DotaAppAgent
│ ├── package.json
│ ├── entities
│ │ └── Hero.json
│ ├── intents
│ │ ├── EndApp.json
│ │ ├── TopHero.json
│ │ ├── EndApp_usersays_en.json
│ │ ├── TopHero_usersays_pt-br.json
│ │ ├── EndApp_usersays_pt-br.json
│ │ ├── Default Welcome Intent.json
│ │ ├── TopHero_usersays_en.json
│ │ ├── BestHero.json
│ │ ├── Default Fallback Intent.json
│ │ ├── Default Welcome Intent_usersays_pt-br.json
│ │ ├── BestHero_usersays_pt-br.json
│ │ ├── BestHero_usersays_en.json
│ │ └── Default Welcome Intent_usersays_en.json
│ └── agent.json
└── DotaAppAgent.zip
├── .firebaserc
├── flutter_dota_app
├── ios
│ ├── Runner
│ │ ├── Runner-Bridging-Header.h
│ │ ├── Assets.xcassets
│ │ │ ├── LaunchImage.imageset
│ │ │ │ ├── LaunchImage.png
│ │ │ │ ├── LaunchImage@2x.png
│ │ │ │ ├── LaunchImage@3x.png
│ │ │ │ ├── README.md
│ │ │ │ └── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ ├── Icon-App-20x20@1x.png
│ │ │ │ ├── Icon-App-20x20@2x.png
│ │ │ │ ├── Icon-App-20x20@3x.png
│ │ │ │ ├── Icon-App-29x29@1x.png
│ │ │ │ ├── Icon-App-29x29@2x.png
│ │ │ │ ├── Icon-App-29x29@3x.png
│ │ │ │ ├── Icon-App-40x40@1x.png
│ │ │ │ ├── Icon-App-40x40@2x.png
│ │ │ │ ├── Icon-App-40x40@3x.png
│ │ │ │ ├── Icon-App-60x60@2x.png
│ │ │ │ ├── Icon-App-60x60@3x.png
│ │ │ │ ├── Icon-App-76x76@1x.png
│ │ │ │ ├── Icon-App-76x76@2x.png
│ │ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ │ ├── Icon-App-83.5x83.5@2x.png
│ │ │ │ └── Contents.json
│ │ ├── AppDelegate.swift
│ │ ├── Base.lproj
│ │ │ ├── Main.storyboard
│ │ │ └── LaunchScreen.storyboard
│ │ └── Info.plist
│ ├── Flutter
│ │ ├── Debug.xcconfig
│ │ ├── Release.xcconfig
│ │ └── AppFrameworkInfo.plist
│ ├── Runner.xcodeproj
│ │ ├── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ ├── Runner.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── .gitignore
│ ├── Podfile
│ └── Podfile.lock
├── android
│ ├── gradle.properties
│ ├── .gitignore
│ ├── app
│ │ ├── src
│ │ │ ├── main
│ │ │ │ ├── res
│ │ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ │ └── ic_launcher.png
│ │ │ │ │ ├── values
│ │ │ │ │ │ └── styles.xml
│ │ │ │ │ └── drawable
│ │ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── kotlin
│ │ │ │ │ └── com
│ │ │ │ │ │ └── example
│ │ │ │ │ │ └── fb_test
│ │ │ │ │ │ └── MainActivity.kt
│ │ │ │ └── AndroidManifest.xml
│ │ │ ├── debug
│ │ │ │ └── AndroidManifest.xml
│ │ │ └── profile
│ │ │ │ └── AndroidManifest.xml
│ │ └── build.gradle
│ ├── gradle
│ │ └── wrapper
│ │ │ └── gradle-wrapper.properties
│ ├── settings.gradle
│ └── build.gradle
├── .metadata
├── .vscode
│ └── launch.json
├── lib
│ ├── main.dart
│ ├── model
│ │ └── hero.dart
│ ├── components
│ │ └── hero_tile.dart
│ └── pages
│ │ ├── heroes_page.dart
│ │ └── hero_page.dart
├── README.md
├── .gitignore
├── test
│ └── widget_test.dart
├── pubspec.yaml
└── pubspec.lock
├── database.rules.json
├── webapp
├── public
│ ├── robots.txt
│ ├── DotaApp.ico
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── DotaApp@192w.png
│ ├── DotaApp@32w.png
│ ├── manifest.json
│ └── index.html
├── src
│ ├── components
│ │ ├── HeroGrid.js
│ │ ├── AddHeroCard.js
│ │ ├── TabNavigation.js
│ │ ├── SelectHeroDialog.js
│ │ ├── Header.js
│ │ └── HeroCard.js
│ ├── setupTests.js
│ ├── App.test.js
│ ├── index.css
│ ├── theme.js
│ ├── Routes.js
│ ├── index.js
│ ├── App.js
│ ├── atoms
│ │ ├── heroes.js
│ │ ├── navigation.js
│ │ └── builder.js
│ ├── pages
│ │ ├── Heroes.js
│ │ ├── Hero.js
│ │ └── TeamBuilder.js
│ ├── App.css
│ ├── logo.svg
│ └── serviceWorker.js
├── .gitignore
├── package.json
└── README.md
├── .images
├── screenshot01.jpeg
├── screenshot02.jpeg
└── screenshot03.jpeg
├── firebase.json
├── utils
├── export.js
└── db.json
├── CONTRIBUTING.md
├── .gitignore
└── README.md
/functions/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | node_modules/
--------------------------------------------------------------------------------
/go-api/.gitignore:
--------------------------------------------------------------------------------
1 | tmp
2 | dota-app
3 | main
4 | *.out
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0"
3 | }
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "dota-app-8c898"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
--------------------------------------------------------------------------------
/database.rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | ".read": true,
4 | ".write": false
5 | }
6 | }
--------------------------------------------------------------------------------
/webapp/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/.images/screenshot01.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/.images/screenshot01.jpeg
--------------------------------------------------------------------------------
/.images/screenshot02.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/.images/screenshot02.jpeg
--------------------------------------------------------------------------------
/.images/screenshot03.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/.images/screenshot03.jpeg
--------------------------------------------------------------------------------
/webapp/public/DotaApp.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/webapp/public/DotaApp.ico
--------------------------------------------------------------------------------
/webapp/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/webapp/public/favicon.ico
--------------------------------------------------------------------------------
/webapp/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/webapp/public/logo192.png
--------------------------------------------------------------------------------
/webapp/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/webapp/public/logo512.png
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/dialogflow/DotaAppAgent.zip
--------------------------------------------------------------------------------
/webapp/public/DotaApp@192w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/webapp/public/DotaApp@192w.png
--------------------------------------------------------------------------------
/webapp/public/DotaApp@32w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/webapp/public/DotaApp@32w.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/flutter_dota_app/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.enableR8=true
3 | android.useAndroidX=true
4 | android.enableJetifier=true
5 |
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/flutter_dota_app/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
--------------------------------------------------------------------------------
/flutter_dota_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/flutter_dota_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/flutter_dota_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/flutter_dota_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/flutter_dota_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alvarowolfx/gcloud-dota-app/HEAD/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/entities/Hero.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "3a74d25c-48e5-4bb0-81bf-350926d6c157",
3 | "name": "Hero",
4 | "isOverridable": true,
5 | "isEnum": false,
6 | "isRegexp": false,
7 | "automatedExpansion": false,
8 | "allowFuzzyExtraction": false
9 | }
--------------------------------------------------------------------------------
/webapp/src/components/HeroGrid.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Grid from '@material-ui/core/Grid'
4 |
5 | export default function HeroGrid({ style, children }){
6 | return (
7 |
8 | {children}
9 |
10 | )
11 | }
--------------------------------------------------------------------------------
/webapp/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/flutter_dota_app/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
7 |
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/webapp/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render();
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/flutter_dota_app/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: f139b11009aeb8ed2a3a3aa8b0066e482709dde3
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/flutter_dota_app/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/flutter_dota_app/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/flutter_dota_app/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Flutter",
9 | "request": "launch",
10 | "type": "dart"
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/flutter_dota_app/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/webapp/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": {
3 | "rules": "database.rules.json"
4 | },
5 | "emulators": {
6 | "functions": {
7 | "port": 5001
8 | }
9 | },
10 | "hosting": {
11 | "public": "webapp/build",
12 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
13 | "rewrites": [
14 | {
15 | "source": "**",
16 | "destination": "/index.html"
17 | }
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/webapp/.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 |
25 | firebase-config.json
26 |
--------------------------------------------------------------------------------
/flutter_dota_app/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import 'package:dota_app/pages/heroes_page.dart';
4 |
5 | void main() => runApp(DotaApp());
6 |
7 | class DotaApp extends StatelessWidget {
8 | @override
9 | Widget build(BuildContext context) {
10 | return MaterialApp(
11 | title: 'Dota App',
12 | theme: ThemeData(
13 | primarySwatch: Colors.red,
14 | ),
15 | home: HeroesPage(title: 'Heroes'),
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @UIApplicationMain
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | GeneratedPluginRegistrant.register(with: self)
11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/go-api/go.mod:
--------------------------------------------------------------------------------
1 | module com.aviebrantz.dota.api
2 |
3 | go 1.14
4 |
5 | require (
6 | cloud.google.com/go/firestore v1.2.0 // indirect
7 | cloud.google.com/go/storage v1.8.0 // indirect
8 | firebase.google.com/go v3.12.1+incompatible
9 | github.com/gofiber/cors v0.0.3
10 | github.com/gofiber/fiber v1.12.6
11 | github.com/gofiber/logger v0.0.8
12 | github.com/gofiber/recover v0.0.5
13 | github.com/joho/godotenv v1.3.0
14 | github.com/klauspost/compress v1.10.5 // indirect
15 | go.uber.org/zap v1.15.0
16 | )
17 |
--------------------------------------------------------------------------------
/flutter_dota_app/android/app/src/main/kotlin/com/example/fb_test/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.aviebrantz.dota_app;
2 |
3 | import androidx.annotation.NonNull;
4 | import io.flutter.embedding.android.FlutterActivity
5 | import io.flutter.embedding.engine.FlutterEngine
6 | import io.flutter.plugins.GeneratedPluginRegistrant
7 |
8 | class MainActivity: FlutterActivity() {
9 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
10 | GeneratedPluginRegistrant.registerWith(flutterEngine);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/flutter_dota_app/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/webapp/src/theme.js:
--------------------------------------------------------------------------------
1 | import { red } from '@material-ui/core/colors';
2 | import { createMuiTheme } from '@material-ui/core/styles';
3 |
4 | const theme = createMuiTheme({
5 | palette: {
6 | primary: {
7 | main: red[800],
8 | },
9 | secondary: {
10 | main: '#FFF',
11 | },
12 | error: {
13 | main: red.A400,
14 | },
15 | background: {
16 | default: '#fff',
17 | },
18 | },
19 | props : {
20 | MuiButtonBase: {
21 | disableRipple: true, // No more ripple
22 | },
23 | }
24 | });
25 |
26 | export default theme;
--------------------------------------------------------------------------------
/flutter_dota_app/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
4 |
5 | def plugins = new Properties()
6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
7 | if (pluginsFile.exists()) {
8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
9 | }
10 |
11 | plugins.each { name, path ->
12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
13 | include ":$name"
14 | project(":$name").projectDir = pluginDirectory
15 | }
16 |
--------------------------------------------------------------------------------
/webapp/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Dota App",
3 | "name": "Dota App",
4 | "icons": [
5 | {
6 | "src": "DotaApp@32w.png",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "DotaApp@192w.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "DotaApp.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#c62828",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/go-api/.gcloudignore:
--------------------------------------------------------------------------------
1 | # This file specifies files that are *not* uploaded to Google Cloud Platform
2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of
3 | # "#!include" directives (which insert the entries of the given .gitignore-style
4 | # file at that point).
5 | #
6 | # For more information, run:
7 | # $ gcloud topic gcloudignore
8 | #
9 | .gcloudignore
10 | # If you would like to upload your .git directory, .gitignore file or files
11 | # from your .gitignore file, remove the corresponding line
12 | # below:
13 | .git
14 | .gitignore
15 |
16 | node_modules
17 | tmp
18 | #!include:.gitignore
--------------------------------------------------------------------------------
/flutter_dota_app/README.md:
--------------------------------------------------------------------------------
1 | # dota_app
2 |
3 | A new Flutter project.
4 |
5 | ## Getting Started
6 |
7 | This project is a starting point for a Flutter application.
8 |
9 | A few resources to get you started if this is your first Flutter project:
10 |
11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
13 |
14 | For help getting started with Flutter, view our
15 | [online documentation](https://flutter.dev/docs), which offers tutorials,
16 | samples, guidance on mobile development, and a full API reference.
17 |
--------------------------------------------------------------------------------
/webapp/src/Routes.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Switch,
4 | Route,
5 | Redirect
6 | } from "react-router-dom";
7 |
8 | import Heroes from './pages/Heroes'
9 | import Hero from './pages/Hero'
10 | import TeamBuilder from './pages/TeamBuilder'
11 |
12 | export default function Routes() {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
--------------------------------------------------------------------------------
/flutter_dota_app/ios/.gitignore:
--------------------------------------------------------------------------------
1 | *.mode1v3
2 | *.mode2v3
3 | *.moved-aside
4 | *.pbxuser
5 | *.perspectivev3
6 | **/*sync/
7 | .sconsign.dblite
8 | .tags*
9 | **/.vagrant/
10 | **/DerivedData/
11 | Icon?
12 | **/Pods/
13 | **/.symlinks/
14 | profile
15 | xcuserdata
16 | **/.generated/
17 | Flutter/App.framework
18 | Flutter/Flutter.framework
19 | Flutter/Flutter.podspec
20 | Flutter/Generated.xcconfig
21 | Flutter/app.flx
22 | Flutter/app.zip
23 | Flutter/flutter_assets/
24 | Flutter/flutter_export_environment.sh
25 | ServiceDefinitions.json
26 | Runner/GeneratedPluginRegistrant.*
27 |
28 | # Exceptions to above rules.
29 | !default.mode1v3
30 | !default.mode2v3
31 | !default.pbxuser
32 | !default.perspectivev3
33 |
--------------------------------------------------------------------------------
/flutter_dota_app/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.3.50'
3 | repositories {
4 | google()
5 | jcenter()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:3.5.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | jcenter()
18 | }
19 | }
20 |
21 | rootProject.buildDir = '../build'
22 | subprojects {
23 | project.buildDir = "${rootProject.buildDir}/${project.name}"
24 | }
25 | subprojects {
26 | project.evaluationDependsOn(':app')
27 | }
28 |
29 | task clean(type: Delete) {
30 | delete rootProject.buildDir
31 | }
32 |
--------------------------------------------------------------------------------
/go-api/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use the offical Golang image to create a build artifact.
2 | # This is based on Debian and sets the GOPATH to /go.
3 | # https://hub.docker.com/_/golang
4 | FROM golang:1.12 as builder
5 |
6 | # Copy local code to the container image.
7 | WORKDIR /go/app
8 | COPY . .
9 |
10 | # Build the command inside the container.
11 | # (You may fetch or manage dependencies here,
12 | # either manually or with a tool like "godep".)
13 | RUN CGO_ENABLED=0 GOOS=linux go build -v -o app main.go
14 |
15 | # Use a Docker multi-stage build to create a lean production image.
16 | # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
17 | FROM gcr.io/distroless/base
18 | COPY --from=builder /go/app/app /app
19 | CMD ["/app"]
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/intents/EndApp.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "a0ecf484-5906-44bd-a1b7-0b86af8ea3fc",
3 | "name": "EndApp",
4 | "auto": true,
5 | "contexts": [],
6 | "responses": [
7 | {
8 | "resetContexts": false,
9 | "affectedContexts": [],
10 | "parameters": [],
11 | "messages": [
12 | {
13 | "type": 0,
14 | "lang": "en",
15 | "condition": "",
16 | "speech": []
17 | }
18 | ],
19 | "defaultResponsePlatforms": {},
20 | "speech": []
21 | }
22 | ],
23 | "priority": 500000,
24 | "webhookUsed": false,
25 | "webhookForSlotFilling": false,
26 | "fallbackIntent": false,
27 | "events": [],
28 | "conditionalResponses": [],
29 | "condition": "",
30 | "conditionalFollowupEvents": []
31 | }
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/intents/TopHero.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "456d3a66-7ece-4c9c-b78c-992d49d7f4d2",
3 | "name": "TopHero",
4 | "auto": true,
5 | "contexts": [],
6 | "responses": [
7 | {
8 | "resetContexts": false,
9 | "affectedContexts": [],
10 | "parameters": [],
11 | "messages": [
12 | {
13 | "type": 0,
14 | "lang": "pt-br",
15 | "condition": "",
16 | "speech": []
17 | }
18 | ],
19 | "defaultResponsePlatforms": {},
20 | "speech": []
21 | }
22 | ],
23 | "priority": 500000,
24 | "webhookUsed": true,
25 | "webhookForSlotFilling": false,
26 | "fallbackIntent": false,
27 | "events": [],
28 | "conditionalResponses": [],
29 | "condition": "",
30 | "conditionalFollowupEvents": []
31 | }
--------------------------------------------------------------------------------
/webapp/src/components/AddHeroCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Grid from '@material-ui/core/Grid'
4 | import Paper from '@material-ui/core/Paper'
5 | import AddIcon from '@material-ui/icons/Add'
6 |
7 | const paperStyle = {
8 | height : 120
9 | }
10 |
11 | const innerCardStyle = {
12 | display : 'flex',
13 | backgroundColor : 'lightgray',
14 | alignItems : 'center',
15 | height : 120
16 | }
17 |
18 | export default function AddHeroCard({ onClick }){
19 | return (
20 |
22 |
23 |
26 |
27 |
28 | )
29 | }
--------------------------------------------------------------------------------
/flutter_dota_app/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | GoogleService-Info.plist
7 | .DS_Store
8 | .atom/
9 | .buildlog/
10 | .history
11 | .svn/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | .dart_tool/
27 | .flutter-plugins
28 | .flutter-plugins-dependencies
29 | .packages
30 | .pub-cache/
31 | .pub/
32 | /build/
33 |
34 | # Web related
35 | lib/generated_plugin_registrant.dart
36 |
37 | # Exceptions to above rules.
38 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
39 |
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/intents/EndApp_usersays_en.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "ac2cc7bf-5988-4771-9557-6a09afc19a56",
4 | "data": [
5 | {
6 | "text": "See ya",
7 | "userDefined": false
8 | }
9 | ],
10 | "isTemplate": false,
11 | "count": 0,
12 | "updated": 0
13 | },
14 | {
15 | "id": "ce17bd47-a571-4fc5-a07e-60619ad6edfd",
16 | "data": [
17 | {
18 | "text": "Bye bye",
19 | "userDefined": false
20 | }
21 | ],
22 | "isTemplate": false,
23 | "count": 0,
24 | "updated": 0
25 | },
26 | {
27 | "id": "9997a7bf-c33f-402a-9ac2-b9b178876744",
28 | "data": [
29 | {
30 | "text": "Ok, thanks",
31 | "userDefined": false
32 | }
33 | ],
34 | "isTemplate": false,
35 | "count": 0,
36 | "updated": 0
37 | }
38 | ]
--------------------------------------------------------------------------------
/utils/export.js:
--------------------------------------------------------------------------------
1 |
2 | const heroes = require('../dota-export.json')
3 | const exampleOutput = [
4 | {
5 | "value": "drow-ranger",
6 | "synonyms": [
7 | "Drow Ranger",
8 | "Drow"
9 | ]
10 | },
11 | {
12 | "value": "abaddon",
13 | "synonyms": [
14 | "Abaddon"
15 | ]
16 | }
17 | ]
18 |
19 | const words = new Set()
20 |
21 | const output = Object.keys(heroes).map( id => {
22 | const { name } = heroes[id]
23 | const synonyms = new Set()
24 | synonyms.add(name)
25 | const parts = name.split(' ')
26 | parts.forEach( p => {
27 | if(!words.has(p)){
28 | synonyms.add(p)
29 | words.add(p)
30 | }
31 | })
32 | return {
33 | value : id,
34 | synonyms : Array.from(synonyms.values())
35 | }
36 | })
37 |
38 | console.log(JSON.stringify(output, null, 2))
--------------------------------------------------------------------------------
/go-api/database/firebase.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "context"
5 | "log"
6 | "os"
7 |
8 | firebase "firebase.google.com/go"
9 | "firebase.google.com/go/db"
10 | )
11 |
12 | func Connect() *db.Client {
13 | projectID := os.Getenv("GCP_PROJECT")
14 | if projectID == "" {
15 | // App Engine uses another name
16 | projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")
17 | }
18 |
19 | config := &firebase.Config{
20 | ProjectID: projectID,
21 | DatabaseURL: "https://" + projectID + ".firebaseio.com",
22 | }
23 |
24 | ctx := context.Background()
25 | fbApp, err := firebase.NewApp(ctx, config)
26 | if err != nil {
27 | log.Fatalf("error initializing app: %v\n", err)
28 | }
29 |
30 | firebaseDB, err := fbApp.Database(ctx)
31 | if err != nil {
32 | log.Fatalln("Error initializing database client:", err)
33 | }
34 |
35 | return firebaseDB
36 | }
37 |
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/intents/TopHero_usersays_pt-br.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "56facd36-666a-4147-9710-985d3223e74d",
4 | "data": [
5 | {
6 | "text": "Que heroi que a galera anda pegando ?",
7 | "userDefined": false
8 | }
9 | ],
10 | "isTemplate": false,
11 | "count": 0,
12 | "updated": 0
13 | },
14 | {
15 | "id": "cc26e6d8-1c8b-4421-add8-2278d06545ef",
16 | "data": [
17 | {
18 | "text": "Qual heroi tá em alta ?",
19 | "userDefined": false
20 | }
21 | ],
22 | "isTemplate": false,
23 | "count": 0,
24 | "updated": 0
25 | },
26 | {
27 | "id": "2417f11f-0aec-4840-8ea1-23f3c0f46207",
28 | "data": [
29 | {
30 | "text": "Quais herois estão sendo mais usados ?",
31 | "userDefined": false
32 | }
33 | ],
34 | "isTemplate": false,
35 | "count": 0,
36 | "updated": 0
37 | }
38 | ]
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 8.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/webapp/src/components/TabNavigation.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { useHistory, useLocation } from 'react-router-dom'
4 | import BottomNavigation from '@material-ui/core/BottomNavigation'
5 | import BottomNavigationAction from '@material-ui/core/BottomNavigationAction'
6 |
7 | import FavoriteIcon from '@material-ui/icons/Favorite'
8 | import DashboardIcon from '@material-ui/icons/Dashboard'
9 |
10 | export default function TabNavigation(){
11 | const history = useHistory()
12 | const location = useLocation()
13 |
14 | return (
15 | {
20 | history.replace(value)
21 | }}
22 | showLabels
23 | >
24 | } />
25 | } />
26 |
27 | )
28 | }
--------------------------------------------------------------------------------
/webapp/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import CssBaseline from '@material-ui/core/CssBaseline'
5 | import { ThemeProvider } from '@material-ui/core/styles'
6 | import {
7 | RecoilRoot
8 | } from 'recoil'
9 |
10 | import "firebase/database"
11 | import * as firebase from "firebase/app"
12 |
13 | import theme from './theme'
14 | import App from './App';
15 | import * as serviceWorker from './serviceWorker';
16 |
17 | const firebaseConfig = require('./firebase-config.json')
18 | firebase.initializeApp(firebaseConfig)
19 |
20 | ReactDOM.render(
21 |
22 |
23 |
24 |
25 |
26 | ,
27 | document.getElementById('root')
28 | );
29 |
30 | // If you want your app to work offline and load faster, you can change
31 | // unregister() to register() below. Note this comes with some pitfalls.
32 | // Learn more about service workers: https://bit.ly/CRA-PWA
33 | serviceWorker.unregister();
34 |
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dota-app-functions",
3 | "scripts": {
4 | "serve": "firebase emulators:start --only functions",
5 | "shell": "firebase functions:shell",
6 | "start": "npm run shell",
7 | "deployFetchDotaBuffHeroById": "firebase deploy --only functions:fetchDotaBuffHeroById",
8 | "deployScheduledFetchDotaBuffHeroes": "firebase deploy --only functions:scheduledFetchDotaBuffHeroes",
9 | "deployDialogflowFulfilment": "firebase deploy --only functions:dialogflowFirebaseFulfillment",
10 | "logs": "firebase functions:log"
11 | },
12 | "engines": {
13 | "node": "8"
14 | },
15 | "main": "src/index.js",
16 | "dependencies": {
17 | "@google-cloud/pubsub": "^1.7.2",
18 | "actions-on-google": "^2.12.0",
19 | "cheerio": "^1.0.0-rc.3",
20 | "dialogflow-fulfillment": "^0.6.1",
21 | "firebase-admin": "^8.10.0",
22 | "firebase-functions": "^3.6.0",
23 | "node-fetch": "^2.6.0"
24 | },
25 | "devDependencies": {
26 | "firebase-functions-test": "^0.2.0"
27 | },
28 | "author": "Alvaro Viebrantz",
29 | "license": "ISC"
30 | }
31 |
--------------------------------------------------------------------------------
/webapp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webapp",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^4.10.0",
7 | "@material-ui/icons": "^4.9.1",
8 | "@testing-library/jest-dom": "^4.2.4",
9 | "@testing-library/react": "^9.5.0",
10 | "@testing-library/user-event": "^7.2.1",
11 | "firebase": "^7.14.5",
12 | "react": "^16.13.1",
13 | "react-dom": "^16.13.1",
14 | "react-router-dom": "^5.2.0",
15 | "react-scripts": "5.0.1",
16 | "recoil": "0.0.8"
17 | },
18 | "scripts": {
19 | "start": "react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test",
22 | "eject": "react-scripts eject",
23 | "deploy": "npm run build && firebase deploy --only hosting"
24 | },
25 | "eslintConfig": {
26 | "extends": "react-app"
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/intents/EndApp_usersays_pt-br.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "4393a99b-536d-42c1-b86d-f4d4a989b26e",
4 | "data": [
5 | {
6 | "text": "Valeu, falou",
7 | "userDefined": false
8 | }
9 | ],
10 | "isTemplate": false,
11 | "count": 0,
12 | "updated": 0
13 | },
14 | {
15 | "id": "ae8fdbbe-be10-4404-8366-978fae2d1c9a",
16 | "data": [
17 | {
18 | "text": "Falou",
19 | "meta": "@sys.ignore",
20 | "userDefined": false
21 | }
22 | ],
23 | "isTemplate": false,
24 | "count": 0,
25 | "updated": 0
26 | },
27 | {
28 | "id": "8b3d72eb-7750-4113-bd28-5d94fa16c9dc",
29 | "data": [
30 | {
31 | "text": "Valeu",
32 | "userDefined": false
33 | }
34 | ],
35 | "isTemplate": false,
36 | "count": 0,
37 | "updated": 0
38 | },
39 | {
40 | "id": "9e06ca5f-72a9-4ec3-8e1a-3c17bdac2a12",
41 | "data": [
42 | {
43 | "text": "Ok, obrigado",
44 | "userDefined": false
45 | }
46 | ],
47 | "isTemplate": false,
48 | "count": 0,
49 | "updated": 0
50 | }
51 | ]
--------------------------------------------------------------------------------
/go-api/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "os"
5 | "strconv"
6 | "time"
7 |
8 | "github.com/gofiber/fiber"
9 | fiberLogger "github.com/gofiber/logger"
10 | "go.uber.org/zap"
11 | )
12 |
13 | func NewZapLogger() func(*fiber.Ctx) {
14 | logConfig := "json"
15 | if logConfigEnv := os.Getenv("LOG_CONFIG"); logConfigEnv != "" {
16 | logConfig = logConfigEnv
17 | }
18 |
19 | if logConfig == "stdout" {
20 | return fiberLogger.New()
21 | }
22 |
23 | logger, _ := zap.NewProduction()
24 |
25 | return func(c *fiber.Ctx) {
26 | defer logger.Sync()
27 | start := time.Now()
28 |
29 | c.Next()
30 |
31 | stop := time.Now()
32 |
33 | timestamp := time.Now().Format(time.RFC3339)
34 | ip := c.IP()
35 | method := c.Method()
36 | path := c.Path()
37 | latency := stop.Sub(start).String()
38 | status := strconv.Itoa(c.Fasthttp.Response.StatusCode())
39 |
40 | logger.Info("REQUEST",
41 | zap.String("timestamp", timestamp),
42 | zap.String("method", method),
43 | zap.String("status", status),
44 | zap.String("ip", ip),
45 | zap.String("path", path),
46 | zap.String("latency", latency),
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/webapp/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './App.css';
3 |
4 | import { useTheme } from '@material-ui/core/styles'
5 | import useMediaQuery from '@material-ui/core/useMediaQuery'
6 |
7 | import Routes from './Routes'
8 | import Header from './components/Header'
9 | import TabNavigation from './components/TabNavigation'
10 |
11 | import { useHeroesList } from './atoms/heroes'
12 | import { NavigationRouter } from './atoms/navigation'
13 |
14 | function App() {
15 | useHeroesList()
16 |
17 | const theme = useTheme()
18 | const isMobile = useMediaQuery(theme.breakpoints.down('xs'))
19 |
20 | return (
21 |
22 |
23 |
24 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | export default App;
41 |
--------------------------------------------------------------------------------
/flutter_dota_app/test/widget_test.dart:
--------------------------------------------------------------------------------
1 | // This is a basic Flutter widget test.
2 | //
3 | // To perform an interaction with a widget in your test, use the WidgetTester
4 | // utility that Flutter provides. For example, you can send tap and scroll
5 | // gestures. You can also use WidgetTester to find child widgets in the widget
6 | // tree, read text, and verify that the values of widget properties are correct.
7 |
8 | import 'package:flutter/material.dart';
9 | import 'package:flutter_test/flutter_test.dart';
10 |
11 | import 'package:dota_app/main.dart';
12 |
13 | void main() {
14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async {
15 | // Build our app and trigger a frame.
16 | await tester.pumpWidget(DotaApp());
17 |
18 | // Verify that our counter starts at 0.
19 | expect(find.text('0'), findsOneWidget);
20 | expect(find.text('1'), findsNothing);
21 |
22 | // Tap the '+' icon and trigger a frame.
23 | await tester.tap(find.byIcon(Icons.add));
24 | await tester.pump();
25 |
26 | // Verify that our counter has incremented.
27 | expect(find.text('0'), findsNothing);
28 | expect(find.text('1'), findsOneWidget);
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/intents/Default Welcome Intent.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "1aa1a121-f67b-4e1d-a9e7-4373ec00672f",
3 | "name": "Default Welcome Intent",
4 | "auto": true,
5 | "contexts": [],
6 | "responses": [
7 | {
8 | "resetContexts": false,
9 | "action": "input.welcome",
10 | "affectedContexts": [],
11 | "parameters": [],
12 | "messages": [
13 | {
14 | "type": 0,
15 | "lang": "en",
16 | "condition": "",
17 | "speech": [
18 | "Hello, seems like a nice day for a Dota match.",
19 | "Hey, let\u0027s play the Dota ?"
20 | ]
21 | },
22 | {
23 | "type": 0,
24 | "lang": "pt-br",
25 | "condition": "",
26 | "speech": [
27 | "Olá, bem vindo ao app do Dota!",
28 | "Oi! vamos jogar um Dota ?"
29 | ]
30 | }
31 | ],
32 | "defaultResponsePlatforms": {},
33 | "speech": []
34 | }
35 | ],
36 | "priority": 500000,
37 | "webhookUsed": false,
38 | "webhookForSlotFilling": false,
39 | "fallbackIntent": false,
40 | "events": [
41 | {
42 | "name": "WELCOME"
43 | }
44 | ],
45 | "conditionalResponses": [],
46 | "condition": "",
47 | "conditionalFollowupEvents": []
48 | }
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/intents/TopHero_usersays_en.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "182f2d32-272a-4fbb-adee-e25d00332770",
4 | "data": [
5 | {
6 | "text": "which heroes are on ",
7 | "userDefined": false
8 | },
9 | {
10 | "text": "the",
11 | "meta": "@sys.ignore",
12 | "userDefined": false
13 | },
14 | {
15 | "text": " top rank ?",
16 | "userDefined": false
17 | }
18 | ],
19 | "isTemplate": false,
20 | "count": 0,
21 | "updated": 0
22 | },
23 | {
24 | "id": "8af7e942-d130-4934-a822-181d1b37e52f",
25 | "data": [
26 | {
27 | "text": "what are ",
28 | "userDefined": false
29 | },
30 | {
31 | "text": "the",
32 | "meta": "@sys.ignore",
33 | "userDefined": false
34 | },
35 | {
36 | "text": " top rank heroes ?",
37 | "userDefined": false
38 | }
39 | ],
40 | "isTemplate": false,
41 | "count": 0,
42 | "updated": 0
43 | },
44 | {
45 | "id": "f0306436-6a70-46a6-bc44-8ae1465766bc",
46 | "data": [
47 | {
48 | "text": "which heroes people have been using ?",
49 | "userDefined": false
50 | }
51 | ],
52 | "isTemplate": false,
53 | "count": 1,
54 | "updated": 0
55 | }
56 | ]
--------------------------------------------------------------------------------
/go-api/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 |
7 | "com.aviebrantz.dota.api/controllers"
8 | "com.aviebrantz.dota.api/database"
9 | "com.aviebrantz.dota.api/logger"
10 | "com.aviebrantz.dota.api/model"
11 | "github.com/gofiber/cors"
12 | "github.com/gofiber/fiber"
13 | "github.com/gofiber/recover"
14 | "github.com/joho/godotenv"
15 | )
16 |
17 | func main() {
18 | err := godotenv.Load()
19 | if err != nil {
20 | log.Println("Error loading .env file")
21 | }
22 |
23 | port := "8000"
24 | if envPort := os.Getenv("PORT"); envPort != "" {
25 | port = envPort
26 | }
27 |
28 | log.Println(port)
29 |
30 | firebaseDB := database.Connect()
31 | heroRepository := model.NewFirebaseHeroRepository(firebaseDB)
32 | heroController := controllers.NewHeroController(heroRepository)
33 |
34 | app := fiber.New()
35 |
36 | app.Use(cors.New())
37 | app.Use(logger.NewZapLogger())
38 | cfg := recover.Config{
39 | Handler: func(c *fiber.Ctx, err error) {
40 | c.Status(500).JSON(fiber.Map{"message": err.Error()})
41 | },
42 | }
43 | app.Use(recover.New(cfg))
44 |
45 | app.Get("/", func(c *fiber.Ctx) {
46 | c.Send("Olá Twitch.tv!")
47 | })
48 |
49 | app.Get("/hero/:heroId", heroController.GetHeroById)
50 | app.Get("/recommendation", heroController.GetHeroesRecommendations)
51 |
52 | err = app.Listen(port)
53 | if err != nil {
54 | log.Fatalf("error initializing app: %v\n", err)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribuiting
2 |
3 | ## Setting up
4 |
5 | 1. [Create a GitHub account](https://help.github.com/articles/signing-up-for-a-new-github-account/) if you don't already have one.
6 | 2. [Install and set up Git](https://help.github.com/articles/set-up-git/).
7 |
8 | ## Forking
9 |
10 | 1. Create your own fork of the [gcloud-flutter-dota-app repository](https://github.com//alvarowolfx/gcloud-flutter-dota-app) by clicking "Fork" in the Web UI. During local development, this will be referred to by `git` as `origin`.
11 |
12 | 2. Download your fork to a local repository.
13 |
14 | ```shell
15 | git clone git@github.com:/gcloud-flutter-dota-app.git
16 | ```
17 |
18 | 3. Add an alias called `upstream` to refer to the main `alvarowolfx/gcloud-flutter-dota-app` repository. Go to the root directory of the newly created local repository directory and run:
19 |
20 | ```shell
21 | git remote add upstream git@github.com:alvarowolfx/gcloud-flutter-dota-app.git
22 | ```
23 |
24 | 4. Fetch data from the `upstream` remote:
25 |
26 | ```shell
27 | git fetch upstream master
28 | ```
29 |
30 | 5. Set up your local `master` branch to track `upstream/master` instead of `origin/master` (which will rapidly become outdated).
31 |
32 | ```shell
33 | git branch -u upstream/master master
34 | ```
35 |
36 | ## Branch (do this each time you want a new branch)
37 |
38 | Create and go to the branch:
39 |
40 | ```shell
41 | git checkout -b master
42 | ```
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Databases
2 |
3 | dota-export.json
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | firebase-debug.log*
12 |
13 | # Firebase cache
14 | .firebase/
15 |
16 | # Firebase config
17 |
18 | # Uncomment this if you'd like others to create their own Firebase project.
19 | # For a team working on the same Firebase project(s), it is recommended to leave
20 | # it commented so all members can deploy to the same project(s) in .firebaserc.
21 | # .firebaserc
22 |
23 | # Runtime data
24 | pids
25 | *.pid
26 | *.seed
27 | *.pid.lock
28 |
29 | # Directory for instrumented libs generated by jscoverage/JSCover
30 | lib-cov
31 |
32 | # Coverage directory used by tools like istanbul
33 | coverage
34 |
35 | # nyc test coverage
36 | .nyc_output
37 |
38 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
39 | .grunt
40 |
41 | # Bower dependency directory (https://bower.io/)
42 | bower_components
43 |
44 | # node-waf configuration
45 | .lock-wscript
46 |
47 | # Compiled binary addons (http://nodejs.org/api/addons.html)
48 | build/Release
49 |
50 | # Dependency directories
51 | node_modules/
52 |
53 | # Optional npm cache directory
54 | .npm
55 |
56 | # Optional eslint cache
57 | .eslintcache
58 |
59 | # Optional REPL history
60 | .node_repl_history
61 |
62 | # Output of 'npm pack'
63 | *.tgz
64 |
65 | # Yarn Integrity file
66 | .yarn-integrity
67 |
68 | # dotenv environment variables file
69 | .env
70 |
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/intents/BestHero.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "614bb79a-0586-4eee-b0f6-46f21a8834ac",
3 | "name": "BestHero",
4 | "auto": true,
5 | "contexts": [],
6 | "responses": [
7 | {
8 | "resetContexts": false,
9 | "affectedContexts": [],
10 | "parameters": [
11 | {
12 | "id": "affd899d-ee0d-4a6d-9fab-8df28ed1da95",
13 | "required": true,
14 | "dataType": "@Hero",
15 | "name": "heroes",
16 | "value": "$heroes",
17 | "prompts": [
18 | {
19 | "lang": "en",
20 | "value": "Which hero they got ?"
21 | },
22 | {
23 | "lang": "en",
24 | "value": "Which hero ?"
25 | }
26 | ],
27 | "promptMessages": [],
28 | "noMatchPromptMessages": [],
29 | "noInputPromptMessages": [],
30 | "outputDialogContexts": [],
31 | "isList": true
32 | }
33 | ],
34 | "messages": [
35 | {
36 | "type": 0,
37 | "lang": "pt-br",
38 | "condition": "",
39 | "speech": []
40 | }
41 | ],
42 | "defaultResponsePlatforms": {},
43 | "speech": []
44 | }
45 | ],
46 | "priority": 500000,
47 | "webhookUsed": true,
48 | "webhookForSlotFilling": false,
49 | "fallbackIntent": false,
50 | "events": [],
51 | "conditionalResponses": [],
52 | "condition": "",
53 | "conditionalFollowupEvents": []
54 | }
--------------------------------------------------------------------------------
/webapp/src/atoms/heroes.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { atom, selector, useSetRecoilState } from 'recoil'
3 | import * as firebase from 'firebase/app'
4 |
5 | const heroesState = atom({
6 | key: 'heroesState',
7 | default: {},
8 | });
9 |
10 | export const heroesSelector = selector({
11 | key: 'heroesSelector',
12 | get: ({get}) => {
13 | return get(heroesState)
14 | }
15 | })
16 |
17 | export const isHeroesLoadingState = atom({
18 | key : 'isHeroesLoadingState',
19 | default : false
20 | })
21 |
22 | export const asyncHeroesState = selector({
23 | key: 'asyncHeroesState',
24 | default: {},
25 | get: async ({get}) => {
26 | try {
27 | const heroesSnap = await firebase.database().ref('/heroes').once('value')
28 | const heroes = heroesSnap.val()
29 | return heroes
30 | }catch(err){}
31 | throw new Error('Failed to fetch heroes')
32 | }
33 | },
34 | )
35 |
36 | export function useHeroesList(){
37 | const setHeroes = useSetRecoilState(heroesState)
38 | const setIsHeroesLoading = useSetRecoilState(isHeroesLoadingState)
39 | useEffect( () => {
40 | async function loadHeroes(){
41 | try {
42 | const heroesSnap = await firebase.database().ref('/heroes').once('value')
43 | const heroes = heroesSnap.val()
44 | setHeroes(heroes)
45 | }catch(err){}
46 | setIsHeroesLoading(false)
47 | }
48 | loadHeroes()
49 | setIsHeroesLoading(true)
50 | }, [setHeroes, setIsHeroesLoading])
51 | }
52 |
--------------------------------------------------------------------------------
/webapp/src/atoms/navigation.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/react-in-jsx-scope */
2 | import React, { useEffect } from 'react'
3 | import { atom, selector, useRecoilState } from 'recoil'
4 | import { Router } from "react-router"
5 | import { createBrowserHistory } from "history"
6 |
7 | const navigationState = atom({
8 | key: 'navigationState',
9 | default: ['/builder'],
10 | });
11 |
12 | export const navigationStateSelector = selector({
13 | key: 'navigationStateSelector',
14 | get: ({get}) => {
15 | return get(navigationState)
16 | }
17 | });
18 |
19 | const history = createBrowserHistory()
20 |
21 | export function NavigationRouter({ children }) {
22 | const [navigation, setNavigation] = useRecoilState(navigationState)
23 |
24 | useEffect( () => {
25 | history.listen( (location, action) => {
26 | switch (action) {
27 | case "PUSH":
28 | // first location when app loads and when pushing onto history
29 | setNavigation([...navigation,location])
30 | break;
31 | case "REPLACE":
32 | // only when using history.replace
33 | setNavigation([location])
34 | break;
35 | case "POP": {
36 | // happens when using the back button, or forward button
37 | const nNavigation = [...navigation]
38 | nNavigation.pop()
39 | setNavigation(nNavigation)
40 | break;
41 | }
42 | default: {}
43 | }
44 | })
45 | })
46 |
47 | return (
48 |
49 | {children}
50 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/flutter_dota_app/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
8 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/webapp/src/components/SelectHeroDialog.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | import Dialog from '@material-ui/core/Dialog'
4 | import TextField from '@material-ui/core/TextField'
5 | import DialogTitle from '@material-ui/core/DialogTitle'
6 |
7 | import HeroCard from '../components/HeroCard'
8 | import HeroGrid from '../components/HeroGrid'
9 |
10 | export default function SelectHeroDialog({ heroes, isOpen, onHeroSelected, onClose }){
11 | const [search, setSearch] = useState("")
12 | const filteredHeroes = heroes.filter( hero => {
13 | return hero.name.toLowerCase().includes(search.toLowerCase())
14 | })
15 |
16 | const onDialogClose = () => {
17 | setSearch("")
18 | onClose()
19 | }
20 |
21 | const onSelected = (id) => {
22 | setSearch("")
23 | onHeroSelected(id)
24 | }
25 |
26 | return (
27 |
50 | )
51 | }
--------------------------------------------------------------------------------
/webapp/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { useRecoilValue } from 'recoil'
4 | import { useHistory, useRouteMatch } from 'react-router-dom'
5 |
6 | import AppBar from '@material-ui/core/AppBar'
7 | import Toolbar from '@material-ui/core/Toolbar'
8 | import Typography from '@material-ui/core/Typography'
9 | import IconButton from '@material-ui/core/IconButton'
10 |
11 | import BackIcon from '@material-ui/icons/ArrowBack'
12 |
13 | import { heroesSelector } from '../atoms/heroes'
14 | import { navigationStateSelector } from '../atoms/navigation'
15 |
16 | export default function Header(){
17 | const history = useHistory()
18 | const match = useRouteMatch("/heroes/:heroId")
19 | const navigation = useRecoilValue(navigationStateSelector)
20 | const heroesList = useRecoilValue(heroesSelector)
21 | const heroId = match && match.params && match.params.heroId
22 |
23 | const hasHistory = navigation.length > 1
24 | const canGoBack = hasHistory || !!heroId
25 | let title = 'Dota App'
26 | if(canGoBack){
27 | const hero = heroesList[heroId] || {}
28 | if(hero){
29 | title = hero.name
30 | }
31 | }
32 |
33 | const goBack = () => {
34 | if(hasHistory){
35 | history.goBack()
36 | } else if(!!heroId){
37 | history.replace('/heroes')
38 | }
39 | }
40 |
41 | return (
42 |
43 |
44 | {canGoBack &&
46 |
47 | }
48 |
49 | {title}
50 |
51 |
52 |
53 | )
54 | }
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | Dota 2 App
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | $(FLUTTER_BUILD_NAME)
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(FLUTTER_BUILD_NUMBER)
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UISupportedInterfaceOrientations
30 |
31 | UIInterfaceOrientationPortrait
32 | UIInterfaceOrientationLandscapeLeft
33 | UIInterfaceOrientationLandscapeRight
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UIViewControllerBasedStatusBarAppearance
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/flutter_dota_app/lib/model/hero.dart:
--------------------------------------------------------------------------------
1 | class DotaHero {
2 | DotaHero({
3 | this.id,
4 | this.name,
5 | this.rank,
6 | this.matches,
7 | this.advantage,
8 | this.winRate,
9 | this.imageUrl,
10 | this.bestHeroes,
11 | this.worstHeroes,
12 | });
13 |
14 | String id;
15 | String name;
16 | String imageUrl;
17 | int rank;
18 | int matches;
19 | double advantage;
20 | double winRate;
21 | List bestHeroes;
22 | List worstHeroes;
23 |
24 | factory DotaHero.fromMap(Map value) {
25 | var bestHeroes = List();
26 | var worstHeroes = List();
27 |
28 | if (value['bestHeroes'] != null) {
29 | Map heroesMap = Map.from(value['bestHeroes']);
30 | //print('${value['name']} - ${heroesMap.keys.length}');
31 | heroesMap.keys.forEach((key) {
32 | var hero = DotaHero.fromMap(Map.from(heroesMap[key]));
33 | bestHeroes.add(hero);
34 | });
35 | //print('${value['name']} - ${bestHeroes.length}');
36 | }
37 |
38 | if (value['worstHeroes'] != null) {
39 | Map heroesMap = Map.from(value['worstHeroes']);
40 | heroesMap.keys.forEach((key) {
41 | var hero = DotaHero.fromMap(Map.from(heroesMap[key]));
42 | worstHeroes.add(hero);
43 | });
44 | }
45 |
46 | return DotaHero(
47 | id: value['id'],
48 | name: value['name'],
49 | rank: value['rank'],
50 | winRate: (value['winRate'] + .0),
51 | matches: value['matches'] ?? 0,
52 | advantage: (value['advantage'] == null ? 0 : value['advantage'] + .0),
53 | imageUrl: value['imageUrl'],
54 | bestHeroes: bestHeroes,
55 | worstHeroes: worstHeroes,
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/agent.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "",
3 | "language": "pt-br",
4 | "shortDescription": "",
5 | "examples": "",
6 | "linkToDocs": "",
7 | "disableInteractionLogs": false,
8 | "disableStackdriverLogs": true,
9 | "googleAssistant": {
10 | "googleAssistantCompatible": true,
11 | "project": "",
12 | "welcomeIntentSignInRequired": false,
13 | "startIntents": [
14 | {
15 | "intentId": "614bb79a-0586-4eee-b0f6-46f21a8834ac",
16 | "signInRequired": false
17 | }
18 | ],
19 | "systemIntents": [],
20 | "endIntentIds": [
21 | "a0ecf484-5906-44bd-a1b7-0b86af8ea3fc"
22 | ],
23 | "oAuthLinking": {
24 | "required": false,
25 | "providerId": "",
26 | "authorizationUrl": "",
27 | "tokenUrl": "",
28 | "scopes": "",
29 | "privacyPolicyUrl": "",
30 | "grantType": "AUTH_CODE_GRANT"
31 | },
32 | "voiceType": "MALE_1",
33 | "capabilities": [],
34 | "env": "",
35 | "protocolVersion": "V2",
36 | "autoPreviewEnabled": true,
37 | "isDeviceAgent": false
38 | },
39 | "defaultTimezone": "America/Barbados",
40 | "webhook": {
41 | "url": "",
42 | "username": "",
43 | "headers": {
44 | "": ""
45 | },
46 | "available": false,
47 | "useForDomains": false,
48 | "cloudFunctionsEnabled": false,
49 | "cloudFunctionsInitialized": false
50 | },
51 | "isPrivate": true,
52 | "customClassifierMode": "use.after",
53 | "mlMinConfidence": 0.3,
54 | "supportedLanguages": [
55 | "en"
56 | ],
57 | "onePlatformApiVersion": "v2",
58 | "analyzeQueryTextSentiment": false,
59 | "enabledKnowledgeBaseNames": [],
60 | "knowledgeServiceConfidenceAdjustment": -0.4,
61 | "dialogBuilderMode": false,
62 | "baseActionPackagesUrl": ""
63 | }
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/intents/Default Fallback Intent.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "d89a7eec-8911-4d90-932a-313a8ff87282",
3 | "name": "Default Fallback Intent",
4 | "auto": true,
5 | "contexts": [],
6 | "responses": [
7 | {
8 | "resetContexts": false,
9 | "action": "input.unknown",
10 | "affectedContexts": [],
11 | "parameters": [],
12 | "messages": [
13 | {
14 | "type": 0,
15 | "lang": "en",
16 | "condition": "",
17 | "speech": [
18 | "I didn\u0027t get that. Can you say it again?",
19 | "I missed what you said. What was that?",
20 | "Sorry, could you say that again?",
21 | "Sorry, can you say that again?",
22 | "Can you say that again?",
23 | "Sorry, I didn\u0027t get that. Can you rephrase?",
24 | "Sorry, what was that?",
25 | "One more time?",
26 | "What was that?",
27 | "Say that one more time?",
28 | "I didn\u0027t get that. Can you repeat?",
29 | "I missed that, say that again?"
30 | ]
31 | },
32 | {
33 | "type": 0,
34 | "lang": "pt-br",
35 | "condition": "",
36 | "speech": [
37 | "Lamento, mas não compreendi.",
38 | "Desculpe, mas não compreendi.",
39 | "Infelizmente, não captei o que deseja.",
40 | "Não consegui compreender, desculpe."
41 | ]
42 | }
43 | ],
44 | "defaultResponsePlatforms": {},
45 | "speech": []
46 | }
47 | ],
48 | "priority": 500000,
49 | "webhookUsed": false,
50 | "webhookForSlotFilling": false,
51 | "fallbackIntent": true,
52 | "events": [],
53 | "conditionalResponses": [],
54 | "condition": "",
55 | "conditionalFollowupEvents": []
56 | }
--------------------------------------------------------------------------------
/webapp/src/components/HeroCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Grid from '@material-ui/core/Grid'
4 | import Paper from '@material-ui/core/Paper'
5 | import Typography from '@material-ui/core/Typography'
6 | import IconButton from '@material-ui/core/IconButton'
7 |
8 | const paperStyle = {
9 | height : 120,
10 | cursor : 'pointer',
11 | position : 'relative',
12 | display : 'block'
13 | }
14 |
15 | const imgStyle = {
16 | width : '100%',
17 | height : '100%',
18 | objectFit : 'cover'
19 | }
20 |
21 | const titlebarStyle = {
22 | display:'flex',
23 | flexDirection : 'row',
24 | position : 'absolute',
25 | left : 0,
26 | bottom : 0,
27 | right : 0,
28 | height : 50,
29 | backgroundColor : 'rgba(0,0,0,0.5)',
30 | justifyContent : 'space-between',
31 | padding : 8,
32 | }
33 |
34 | const whiteBoldTextStyle = {
35 | fontWeight : 'bold',
36 | color : 'white'
37 | }
38 |
39 | const whiteTextStyle = {
40 | color : 'white'
41 | }
42 |
43 | export default function HeroCard({ hero, actionIcon, onActionClick, ...rest }){
44 | return (
45 |
46 |
47 |
48 |
49 |
50 | {hero.name}
51 | Rank: {hero.rank}
52 |
53 | {actionIcon &&
onActionClick(hero.id)}>
55 | {actionIcon}
56 | }
57 |
58 |
59 |
60 | )
61 | }
--------------------------------------------------------------------------------
/flutter_dota_app/lib/components/hero_tile.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:dota_app/model/hero.dart';
3 | import 'package:cached_network_image/cached_network_image.dart';
4 |
5 | class HeroTile extends StatelessWidget {
6 | HeroTile(this.hero, {Key key, this.onHeroSelected, this.onTap})
7 | : super(key: key);
8 |
9 | final DotaHero hero;
10 | final Function(DotaHero) onHeroSelected;
11 | final VoidCallback onTap;
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | return GestureDetector(
16 | onTap: () {
17 | if (this.onTap != null) {
18 | onTap();
19 | }
20 | if (this.onHeroSelected != null) {
21 | this.onHeroSelected(this.hero);
22 | }
23 | },
24 | child: Stack(
25 | children: [
26 | Container(
27 | decoration: BoxDecoration(
28 | image: DecorationImage(
29 | image: CachedNetworkImageProvider(
30 | hero.imageUrl,
31 | ),
32 | fit: BoxFit.cover,
33 | ),
34 | ),
35 | ),
36 | Container(
37 | decoration: BoxDecoration(
38 | gradient: LinearGradient(
39 | begin: Alignment.topCenter,
40 | end: Alignment.bottomCenter,
41 | stops: [0.7, 1],
42 | colors: [Colors.black12, Colors.black]),
43 | ),
44 | ),
45 | Container(
46 | alignment: Alignment.bottomRight,
47 | padding: EdgeInsets.all(8),
48 | child: Text(
49 | hero.name,
50 | style: Theme.of(context)
51 | .textTheme
52 | .subtitle
53 | .copyWith(color: Colors.white),
54 | ),
55 | )
56 | ],
57 | ),
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/webapp/src/pages/Heroes.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useRecoilValue } from 'recoil'
3 | import { useHistory } from 'react-router-dom'
4 |
5 | import TextField from '@material-ui/core/TextField'
6 | import Container from '@material-ui/core/Container'
7 | import CircularProgress from '@material-ui/core/CircularProgress'
8 |
9 | import HeroCard from '../components/HeroCard'
10 | import HeroGrid from '../components/HeroGrid'
11 |
12 | import { heroesSelector, isHeroesLoadingState } from '../atoms/heroes'
13 |
14 | export default function Heroes(){
15 | const heroesList = useRecoilValue(heroesSelector)
16 | const isLoading = useRecoilValue(isHeroesLoadingState)
17 | const history = useHistory()
18 |
19 | const [search, setSearch] = useState("")
20 | const filteredHeroes = Object.values(heroesList).filter( hero => {
21 | return hero.name.toLowerCase().includes(search.toLowerCase())
22 | })
23 |
24 | const goToHero = (id) => {
25 | history.push(`/heroes/${id}`)
26 | }
27 |
28 | return (
29 |
30 | setSearch(evt.target.value)} />
37 | {isLoading &&
38 |
39 |
40 |
}
41 | {!isLoading && (
42 |
43 | {filteredHeroes.map( hero => {
44 | return (
45 | goToHero(hero.id)} />
49 | )
50 | })}
51 |
52 | )}
53 |
54 | )
55 | }
--------------------------------------------------------------------------------
/webapp/src/App.css:
--------------------------------------------------------------------------------
1 | html,
2 | #root,
3 | .AppContainer {
4 | height: 100%;
5 | overflow: hidden;
6 | }
7 |
8 | body {
9 | /*background-color: #c62828 !important;*/
10 | height: 100%;
11 | overflow: hidden;
12 |
13 | padding-bottom: 0px;
14 | }
15 |
16 | .tabNavigation {
17 | padding-bottom: 0px;
18 | -webkit-box-shadow: 2px -2px 7px 1px rgba(0, 0, 0, 0.25);
19 | -moz-box-shadow: 2px -2px 7px 1px rgba(0, 0, 0, 0.25);
20 | box-shadow: 2px -2px 7px 1px rgba(0, 0, 0, 0.25);
21 | }
22 |
23 | .HeroTabs {
24 | height: calc(100% - 240px - 56px);
25 | }
26 |
27 | @supports (padding-top: env(safe-area-inset-top)) {
28 | .AppBar {
29 | --safe-area-inset-top: env(safe-area-inset-top);
30 | padding-top: env(safe-area-inset-top);
31 | }
32 | }
33 | @supports (padding-top: env(safe-area-inset-top)) {
34 | .AppContainer {
35 | margin-top: calc(56px + env(safe-area-inset-top)) !important;
36 | overflow: hidden;
37 | padding-bottom: calc(56px + 56px + env(safe-area-inset-bottom)) !important;
38 | }
39 | }
40 |
41 | @supports (padding-bottom: env(safe-area-inset-bottom)) {
42 | .HeroTabs {
43 | height: calc(100% - 240px - 32px - env(safe-area-inset-bottom));
44 | }
45 | }
46 |
47 | .App {
48 | background-color: white;
49 | height: 100%;
50 | overflow: hidden;
51 | }
52 |
53 | .App-logo {
54 | height: 40vmin;
55 | pointer-events: none;
56 | }
57 |
58 | @media (prefers-reduced-motion: no-preference) {
59 | .App-logo {
60 | animation: App-logo-spin infinite 20s linear;
61 | }
62 | }
63 |
64 | .App-header {
65 | background-color: #282c34;
66 | min-height: 100vh;
67 | display: flex;
68 | flex-direction: column;
69 | align-items: center;
70 | justify-content: center;
71 | font-size: calc(10px + 2vmin);
72 | color: white;
73 | }
74 |
75 | .App-link {
76 | color: #61dafb;
77 | }
78 |
79 | @keyframes App-logo-spin {
80 | from {
81 | transform: rotate(0deg);
82 | }
83 | to {
84 | transform: rotate(360deg);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/flutter_dota_app/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply plugin: 'kotlin-android'
26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
27 |
28 | android {
29 | compileSdkVersion 28
30 |
31 | sourceSets {
32 | main.java.srcDirs += 'src/main/kotlin'
33 | }
34 |
35 | lintOptions {
36 | disable 'InvalidPackage'
37 | }
38 |
39 | defaultConfig {
40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
41 | applicationId "com.aviebrantz.dota_app"
42 | minSdkVersion 16
43 | targetSdkVersion 28
44 | versionCode flutterVersionCode.toInteger()
45 | versionName flutterVersionName
46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
47 | }
48 |
49 | buildTypes {
50 | release {
51 | // TODO: Add your own signing config for the release build.
52 | // Signing with the debug keys for now, so `flutter run --release` works.
53 | signingConfig signingConfigs.debug
54 | }
55 | }
56 | }
57 |
58 | flutter {
59 | source '../..'
60 | }
61 |
62 | dependencies {
63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
64 | testImplementation 'junit:junit:4.12'
65 | androidTestImplementation 'androidx.test:runner:1.1.1'
66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
67 | }
68 |
--------------------------------------------------------------------------------
/webapp/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
31 |
32 |
36 |
37 |
41 |
42 |
46 |
47 | Dota App
48 |
49 |
50 |
51 |
52 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/webapp/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/intents/Default Welcome Intent_usersays_pt-br.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "eca70350-2abf-4ef9-895b-b20ca35c8b8f",
4 | "data": [
5 | {
6 | "text": "há quanto tempo",
7 | "userDefined": false
8 | }
9 | ],
10 | "isTemplate": false,
11 | "count": 0,
12 | "updated": 0
13 | },
14 | {
15 | "id": "a3a65ab7-bf9d-47e7-b85d-8ee988824539",
16 | "data": [
17 | {
18 | "text": "olá",
19 | "userDefined": false
20 | }
21 | ],
22 | "isTemplate": false,
23 | "count": 0,
24 | "updated": 0
25 | },
26 | {
27 | "id": "f712f5c0-f829-441b-a240-fd4a07982f1f",
28 | "data": [
29 | {
30 | "text": "oi",
31 | "userDefined": false
32 | }
33 | ],
34 | "isTemplate": false,
35 | "count": 0,
36 | "updated": 0
37 | },
38 | {
39 | "id": "7d24502f-54cb-4171-8052-6e22ce3a3df2",
40 | "data": [
41 | {
42 | "text": "opa",
43 | "userDefined": false
44 | }
45 | ],
46 | "isTemplate": false,
47 | "count": 0,
48 | "updated": 0
49 | },
50 | {
51 | "id": "cf14da8a-ef1a-47e4-9315-d5f19bf61c19",
52 | "data": [
53 | {
54 | "text": "fala aí",
55 | "userDefined": false
56 | }
57 | ],
58 | "isTemplate": false,
59 | "count": 0,
60 | "updated": 0
61 | },
62 | {
63 | "id": "25b12229-8ca8-4bcd-b03b-46b2b2d221ec",
64 | "data": [
65 | {
66 | "text": "fala",
67 | "userDefined": false
68 | }
69 | ],
70 | "isTemplate": false,
71 | "count": 0,
72 | "updated": 0
73 | },
74 | {
75 | "id": "e871caf1-53b9-4055-8288-3593cb158f1e",
76 | "data": [
77 | {
78 | "text": "saudações",
79 | "userDefined": false
80 | }
81 | ],
82 | "isTemplate": false,
83 | "count": 0,
84 | "updated": 0
85 | },
86 | {
87 | "id": "7de2da73-47a4-4712-a2b7-8709155a4100",
88 | "data": [
89 | {
90 | "text": "oi tudo bem",
91 | "userDefined": false
92 | }
93 | ],
94 | "isTemplate": false,
95 | "count": 0,
96 | "updated": 0
97 | },
98 | {
99 | "id": "b2c408ad-2f9e-4105-b7ac-8c438439cf4f",
100 | "data": [
101 | {
102 | "text": "e aí",
103 | "userDefined": false
104 | }
105 | ],
106 | "isTemplate": false,
107 | "count": 0,
108 | "updated": 0
109 | },
110 | {
111 | "id": "c481b9e5-c91f-4c20-9879-ca07a5c1bc84",
112 | "data": [
113 | {
114 | "text": "eae",
115 | "userDefined": false
116 | }
117 | ],
118 | "isTemplate": false,
119 | "count": 0,
120 | "updated": 0
121 | }
122 | ]
--------------------------------------------------------------------------------
/go-api/model/hero.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "sync"
7 | "time"
8 |
9 | "firebase.google.com/go/db"
10 | )
11 |
12 | type DotaHeroVersus struct {
13 | Advantage float64 `json:"advantage"`
14 | ID string `json:"id"`
15 | Matches int `json:"matches"`
16 | Name string `json:"name"`
17 | WinRate float64 `json:"winRate"`
18 | }
19 |
20 | type DotaHero struct {
21 | ID string `json:"id"`
22 | ImageURL string `json:"imageUrl"`
23 | Name string `json:"name"`
24 | Rank int `json:"rank"`
25 | WinRate float64 `json:"winRate"`
26 | BestHeroes map[string]DotaHeroVersus `json:"bestHeroes,omitempty"`
27 | WorstHeroes map[string]DotaHeroVersus `json:"worstHeroes,omitempty"`
28 | }
29 |
30 | type ByDotaHeroVersusAdvantage []DotaHeroVersus
31 |
32 | func (s ByDotaHeroVersusAdvantage) Len() int {
33 | return len(s)
34 | }
35 | func (s ByDotaHeroVersusAdvantage) Swap(i, j int) {
36 | s[i], s[j] = s[j], s[i]
37 | }
38 | func (s ByDotaHeroVersusAdvantage) Less(i, j int) bool {
39 | return s[i].Advantage > s[j].Advantage
40 | }
41 |
42 | type HeroRepository interface {
43 | FindById(ctx context.Context, id string) (*DotaHero, error)
44 | LoadHeroesList(ids []string) ([]DotaHero, error)
45 | }
46 |
47 | type FirebaseHeroRepository struct {
48 | FirebaseDB *db.Client
49 | }
50 |
51 | func NewFirebaseHeroRepository(firebaseDB *db.Client) HeroRepository {
52 | return &FirebaseHeroRepository{
53 | FirebaseDB: firebaseDB,
54 | }
55 | }
56 |
57 | func (fhr *FirebaseHeroRepository) FindById(ctx context.Context, id string) (*DotaHero, error) {
58 | hero := &DotaHero{}
59 | err := fhr.FirebaseDB.NewRef("/heroes").Child(id).Get(ctx, hero)
60 | return hero, err
61 | }
62 |
63 | func (fhr *FirebaseHeroRepository) LoadHeroesList(ids []string) ([]DotaHero, error) {
64 | var heroes []DotaHero
65 |
66 | ctx, cancel := context.WithCancel(context.Background())
67 | var wg sync.WaitGroup
68 | var mutex = &sync.Mutex{}
69 |
70 | for i := 0; i < len(ids); i++ {
71 | id := ids[i]
72 | wg.Add(1)
73 | go func(id string, wg *sync.WaitGroup) {
74 | defer wg.Done()
75 | hero, err := fhr.FindById(ctx, id)
76 | if err == nil {
77 | mutex.Lock()
78 | heroes = append(heroes, *hero)
79 | mutex.Unlock()
80 | }
81 | }(id, &wg)
82 | }
83 |
84 | timeout := make(chan bool, 1)
85 | done := make(chan bool, 1)
86 |
87 | go func() {
88 | time.Sleep(5 * time.Second)
89 | cancel()
90 | timeout <- true
91 | }()
92 |
93 | go func() {
94 | wg.Wait()
95 | done <- true
96 | }()
97 |
98 | select {
99 | case <-done:
100 | return heroes, nil
101 | case <-timeout:
102 | return nil, errors.New("Timeout getting heroes data")
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/intents/BestHero_usersays_pt-br.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "fcb9b5d5-1bd5-474a-b363-3c8af4352b7f",
4 | "data": [
5 | {
6 | "text": "Que heroi pego contra a ",
7 | "userDefined": false
8 | },
9 | {
10 | "text": "Drow",
11 | "alias": "heroes",
12 | "meta": "@Hero",
13 | "userDefined": false
14 | },
15 | {
16 | "text": " ?",
17 | "userDefined": false
18 | }
19 | ],
20 | "isTemplate": false,
21 | "count": 0,
22 | "updated": 0
23 | },
24 | {
25 | "id": "472bf042-007a-4530-ace4-7abee26e69c3",
26 | "data": [
27 | {
28 | "text": "Qual herói é bom contra a ",
29 | "userDefined": false
30 | },
31 | {
32 | "text": "Aranha",
33 | "alias": "heroes",
34 | "meta": "@Hero",
35 | "userDefined": false
36 | },
37 | {
38 | "text": " e o ",
39 | "userDefined": false
40 | },
41 | {
42 | "text": "Death Knight",
43 | "alias": "heroes",
44 | "meta": "@Hero",
45 | "userDefined": false
46 | },
47 | {
48 | "text": " ?",
49 | "userDefined": false
50 | }
51 | ],
52 | "isTemplate": false,
53 | "count": 0,
54 | "updated": 0
55 | },
56 | {
57 | "id": "0e187fc5-961c-490a-bf77-feaacdc7a461",
58 | "data": [
59 | {
60 | "text": "O outro time pegou ",
61 | "userDefined": false
62 | },
63 | {
64 | "text": "Windranger",
65 | "alias": "heroes",
66 | "meta": "@Hero",
67 | "userDefined": false
68 | },
69 | {
70 | "text": ", ",
71 | "userDefined": false
72 | },
73 | {
74 | "text": "Huskar",
75 | "alias": "heroes",
76 | "meta": "@Hero",
77 | "userDefined": false
78 | },
79 | {
80 | "text": " e o ",
81 | "userDefined": false
82 | },
83 | {
84 | "text": "Pudge",
85 | "alias": "heroes",
86 | "meta": "@Hero",
87 | "userDefined": false
88 | },
89 | {
90 | "text": ", o que pegar ?",
91 | "userDefined": false
92 | }
93 | ],
94 | "isTemplate": false,
95 | "count": 0,
96 | "updated": 0
97 | },
98 | {
99 | "id": "68f02e8d-d944-4ff9-82b5-15a03d3a9c4b",
100 | "data": [
101 | {
102 | "text": "O inimigo pegou a ",
103 | "userDefined": false
104 | },
105 | {
106 | "text": "Drow",
107 | "alias": "heroes",
108 | "meta": "@Hero",
109 | "userDefined": false
110 | },
111 | {
112 | "text": ", o que eu pego ?",
113 | "userDefined": false
114 | }
115 | ],
116 | "isTemplate": false,
117 | "count": 0,
118 | "updated": 0
119 | }
120 | ]
--------------------------------------------------------------------------------
/flutter_dota_app/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: dota_app
2 | description: A new Flutter project.
3 |
4 | # The following defines the version and build number for your application.
5 | # A version number is three numbers separated by dots, like 1.2.43
6 | # followed by an optional build number separated by a +.
7 | # Both the version and the builder number may be overridden in flutter
8 | # build by specifying --build-name and --build-number, respectively.
9 | # In Android, build-name is used as versionName while build-number used as versionCode.
10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning
11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
12 | # Read more about iOS versioning at
13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
14 | version: 1.0.0+1
15 |
16 | environment:
17 | sdk: ">=2.2.2 <3.0.0"
18 |
19 | dependencies:
20 | flutter:
21 | sdk: flutter
22 |
23 | # The following adds the Cupertino Icons font to your application.
24 | # Use with the CupertinoIcons class for iOS style icons.
25 | cupertino_icons: ^0.1.2
26 | firebase_core: ^0.4.4
27 | firebase_database: ^3.1.5
28 | cached_network_image: ^2.1.0
29 |
30 | dev_dependencies:
31 | flutter_test:
32 | sdk: flutter
33 |
34 | # For information on the generic Dart part of this file, see the
35 | # following page: https://dart.dev/tools/pub/pubspec
36 |
37 | # The following section is specific to Flutter.
38 | flutter:
39 | # The following line ensures that the Material Icons font is
40 | # included with your application, so that you can use the icons in
41 | # the material Icons class.
42 | uses-material-design: true
43 | # To add assets to your application, add an assets section, like this:
44 | # assets:
45 | # - images/a_dot_burr.jpeg
46 | # - images/a_dot_ham.jpeg
47 | # An image asset can refer to one or more resolution-specific "variants", see
48 | # https://flutter.dev/assets-and-images/#resolution-aware.
49 | # For details regarding adding assets from package dependencies, see
50 | # https://flutter.dev/assets-and-images/#from-packages
51 | # To add custom fonts to your application, add a fonts section here,
52 | # in this "flutter" section. Each entry in this list should have a
53 | # "family" key with the font family name, and a "fonts" key with a
54 | # list giving the asset and other descriptors for the font. For
55 | # example:
56 | # fonts:
57 | # - family: Schyler
58 | # fonts:
59 | # - asset: fonts/Schyler-Regular.ttf
60 | # - asset: fonts/Schyler-Italic.ttf
61 | # style: italic
62 | # - family: Trajan Pro
63 | # fonts:
64 | # - asset: fonts/TrajanPro.ttf
65 | # - asset: fonts/TrajanPro_Bold.ttf
66 | # weight: 700
67 | #
68 | # For details regarding fonts from package dependencies,
69 | # see https://flutter.dev/custom-fonts/#from-packages
70 |
--------------------------------------------------------------------------------
/webapp/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `npm run build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/webapp/src/atoms/builder.js:
--------------------------------------------------------------------------------
1 | import { atom, selector, useRecoilValueLoadable, useRecoilState } from 'recoil'
2 |
3 | import { heroesSelector } from './heroes'
4 |
5 | const BASE_URL = "https://dota-recommendation-api-m423ptj4pq-uc.a.run.app"
6 |
7 | const enemyHeroesState = atom({
8 | key: 'enemyHeroesState',
9 | default: [],
10 | });
11 |
12 | const teamHeroesState = atom({
13 | key : 'teamHeroesState',
14 | default : []
15 | })
16 |
17 | export const enemyHeroesSelector = selector({
18 | key: 'enemyHeroesSelector',
19 | get: ({get}) => {
20 | return get(enemyHeroesState)
21 | }
22 | })
23 |
24 | export const teamHeroesSelector = selector({
25 | key: 'teamHeroesSelector',
26 | get: ({get}) => {
27 | return get(teamHeroesState)
28 | }
29 | })
30 |
31 | const recommendedHeroesSelector = selector({
32 | key : 'recommendedHeroesSelector',
33 | get: async({ get, set }) => {
34 | const enemies = get(enemyHeroesState)
35 | const team = get(teamHeroesState)
36 |
37 | if(enemies.length === 0){
38 | return Promise.resolve([])
39 | }
40 |
41 | const url = `${BASE_URL}/recommendation?enemies=${enemies.join(',')}&team=${team.join(',')}`
42 |
43 | const res = await fetch(url)
44 | if(res.ok){
45 | const json = await res.json()
46 | return Object.keys(json)
47 | }
48 | return Promise.resolve([])
49 | }
50 | })
51 |
52 | export function useRecommendedHeroes() {
53 | const recommendedHeroes = useRecoilValueLoadable(recommendedHeroesSelector)
54 | const isRecommendedHeroesLoading = recommendedHeroes.state === 'loading'
55 | const recommendHeroesIds = recommendedHeroes.state === 'hasValue' ? recommendedHeroes.contents : []
56 | return [isRecommendedHeroesLoading, recommendHeroesIds]
57 | }
58 |
59 | export const useTeamBuilderActions = () => {
60 | const [enemies, setEnemies] = useRecoilState(enemyHeroesState)
61 | const [team, setTeam] = useRecoilState(teamHeroesState)
62 |
63 | const addEnemyHero = (id) => {
64 | const enemiesList = enemies
65 | if(enemiesList.length < 5){
66 | const nEnemyList = enemiesList.filter( enemyId => enemyId !== id)
67 | setEnemies([...nEnemyList, id])
68 | }
69 | }
70 |
71 | const removeEnemyHero = (id) => {
72 | const enemiesList = enemies.filter(enemyId => enemyId !== id)
73 | setEnemies([...enemiesList])
74 | }
75 |
76 | const addTeamHero = (id) => {
77 | const teamList = team
78 | if(teamList.length < 5){
79 | const nTeamList = teamList.filter( heroId => heroId !== id)
80 | setTeam([...nTeamList, id])
81 | }
82 | }
83 |
84 | const removeTeamHero = (id) => {
85 | const teamList = team.filter(heroId => heroId !== id)
86 | setTeam([...teamList])
87 | }
88 |
89 | const resetBuilder = () => {
90 | setTeam([])
91 | setEnemies([])
92 | }
93 |
94 | return {
95 | addEnemyHero,
96 | removeEnemyHero,
97 | addTeamHero,
98 | removeTeamHero,
99 | resetBuilder
100 | }
101 | }
102 |
103 | export const availableHeroesIdsSelector = selector({
104 | key: 'availableHeroesSelector',
105 | get: ({get}) => {
106 | const allPickedHeroes = [...get(enemyHeroesState),...get(teamHeroesState)]
107 | const allHeroes = get(heroesSelector)
108 | const heroesIds = Object.keys(allHeroes).filter( heroId => !allPickedHeroes.includes(heroId))
109 | return heroesIds
110 | },
111 | })
112 |
--------------------------------------------------------------------------------
/flutter_dota_app/lib/pages/heroes_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:firebase_database/firebase_database.dart';
3 |
4 | import 'package:dota_app/model/hero.dart';
5 | import 'package:dota_app/components/hero_tile.dart';
6 | import 'package:dota_app/pages/hero_page.dart';
7 |
8 | class HeroesPage extends StatefulWidget {
9 | HeroesPage({Key key, this.title}) : super(key: key);
10 |
11 | final String title;
12 |
13 | @override
14 | _HeroesPageState createState() => _HeroesPageState();
15 | }
16 |
17 | class _HeroesPageState extends State {
18 | DatabaseReference heroesRef;
19 | String sortingMethod = 'name';
20 |
21 | @override
22 | void initState() {
23 | super.initState();
24 | heroesRef = FirebaseDatabase.instance.reference().child('/heroes');
25 | }
26 |
27 | @override
28 | Widget build(BuildContext context) {
29 | return Scaffold(
30 | appBar: AppBar(
31 | title: Text(widget.title),
32 | actions: [
33 | PopupMenuButton(
34 | icon: sortingMethod == 'name'
35 | ? Icon(Icons.sort_by_alpha)
36 | : Icon(Icons.sort),
37 | onSelected: (sortMethod) {
38 | setState(() {
39 | sortingMethod = sortMethod;
40 | });
41 | },
42 | itemBuilder: (BuildContext context) {
43 | return ['name', 'rank'].map((sortMethod) {
44 | return PopupMenuItem(
45 | value: sortMethod, child: Text('Sort by $sortMethod'));
46 | }).toList();
47 | },
48 | ),
49 | ],
50 | ),
51 | body: Center(
52 | child: StreamBuilder(
53 | stream: this.heroesRef.onValue,
54 | builder: (context, snapshot) {
55 | if (!snapshot.hasData) {
56 | return Center(child: CircularProgressIndicator());
57 | }
58 |
59 | Map heroesMap =
60 | Map.from(snapshot.data.snapshot.value);
61 |
62 | var heroes = heroesMap.keys.map((key) {
63 | return DotaHero.fromMap(Map.from(heroesMap[key]));
64 | }).toList();
65 |
66 | if (sortingMethod == 'rank') {
67 | heroes.sort((a, b) => a.rank - b.rank);
68 | } else {
69 | heroes.sort((a, b) => a.name.compareTo(b.name));
70 | }
71 |
72 | return GridView.builder(
73 | padding: EdgeInsets.all(8),
74 | shrinkWrap: true,
75 | itemCount: heroes.length,
76 | itemBuilder: (context, index) {
77 | return HeroTile(
78 | heroes[index],
79 | onHeroSelected: (hero) {
80 | Navigator.push(
81 | context,
82 | MaterialPageRoute(
83 | builder: (context) => HeroPage(hero: hero),
84 | ),
85 | );
86 | },
87 | );
88 | },
89 | gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
90 | childAspectRatio: 2,
91 | crossAxisSpacing: 8,
92 | mainAxisSpacing: 8,
93 | crossAxisCount: 2,
94 | ),
95 | );
96 | },
97 | ),
98 | ),
99 | );
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/intents/BestHero_usersays_en.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "d0e02096-e7c7-4311-b8de-b764a9c1ed77",
4 | "data": [
5 | {
6 | "text": "what should I pick against ",
7 | "userDefined": false
8 | },
9 | {
10 | "text": "Enigma",
11 | "alias": "heroes",
12 | "meta": "@Hero",
13 | "userDefined": false
14 | },
15 | {
16 | "text": " ?",
17 | "userDefined": false
18 | }
19 | ],
20 | "isTemplate": false,
21 | "count": 0,
22 | "updated": 0
23 | },
24 | {
25 | "id": "31c0f717-e819-4493-bfef-5e652c1804ee",
26 | "data": [
27 | {
28 | "text": "Their team got ",
29 | "userDefined": false
30 | },
31 | {
32 | "text": "Enchantress",
33 | "alias": "heroes",
34 | "meta": "@Hero",
35 | "userDefined": false
36 | },
37 | {
38 | "text": ", ",
39 | "userDefined": false
40 | },
41 | {
42 | "text": "Huskar",
43 | "alias": "heroes",
44 | "meta": "@Hero",
45 | "userDefined": false
46 | },
47 | {
48 | "text": " and ",
49 | "userDefined": false
50 | },
51 | {
52 | "text": "Invoker",
53 | "alias": "heroes",
54 | "meta": "@Hero",
55 | "userDefined": false
56 | },
57 | {
58 | "text": ", which hero is a good counter?",
59 | "userDefined": false
60 | }
61 | ],
62 | "isTemplate": false,
63 | "count": 0,
64 | "updated": 0
65 | },
66 | {
67 | "id": "b6e9de48-5145-4c47-80db-5d3134db5f61",
68 | "data": [
69 | {
70 | "text": "Hmmm, ",
71 | "userDefined": false
72 | },
73 | {
74 | "text": "the",
75 | "meta": "@sys.ignore",
76 | "userDefined": false
77 | },
78 | {
79 | "text": " other team just picked ",
80 | "userDefined": false
81 | },
82 | {
83 | "text": "Beastmaster",
84 | "alias": "heroes",
85 | "meta": "@Hero",
86 | "userDefined": false
87 | },
88 | {
89 | "text": ", what should I pick?",
90 | "userDefined": false
91 | }
92 | ],
93 | "isTemplate": false,
94 | "count": 0,
95 | "updated": 0
96 | },
97 | {
98 | "id": "ab43d6f8-6f38-40ca-a701-b8c9c889a8b8",
99 | "data": [
100 | {
101 | "text": "They picked ",
102 | "userDefined": false
103 | },
104 | {
105 | "text": "Abaddon",
106 | "alias": "heroes",
107 | "meta": "@Hero",
108 | "userDefined": false
109 | },
110 | {
111 | "text": " and ",
112 | "userDefined": false
113 | },
114 | {
115 | "text": "Dragon Knight",
116 | "alias": "heroes",
117 | "meta": "@Hero",
118 | "userDefined": false
119 | },
120 | {
121 | "text": ", which hero should I pick?",
122 | "userDefined": false
123 | }
124 | ],
125 | "isTemplate": false,
126 | "count": 0,
127 | "updated": 0
128 | },
129 | {
130 | "id": "a380014a-d59a-4151-bbb6-f04f0026a0fd",
131 | "data": [
132 | {
133 | "text": "Which hero is good against ",
134 | "userDefined": false
135 | },
136 | {
137 | "text": "Drown Ranger",
138 | "alias": "heroes",
139 | "meta": "@Hero",
140 | "userDefined": true
141 | },
142 | {
143 | "text": " ?",
144 | "userDefined": false
145 | }
146 | ],
147 | "isTemplate": false,
148 | "count": 0,
149 | "updated": 0
150 | }
151 | ]
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def parse_KV_file(file, separator='=')
14 | file_abs_path = File.expand_path(file)
15 | if !File.exists? file_abs_path
16 | return [];
17 | end
18 | generated_key_values = {}
19 | skip_line_start_symbols = ["#", "/"]
20 | File.foreach(file_abs_path) do |line|
21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
22 | plugin = line.split(pattern=separator)
23 | if plugin.length == 2
24 | podname = plugin[0].strip()
25 | path = plugin[1].strip()
26 | podpath = File.expand_path("#{path}", file_abs_path)
27 | generated_key_values[podname] = podpath
28 | else
29 | puts "Invalid plugin specification: #{line}"
30 | end
31 | end
32 | generated_key_values
33 | end
34 |
35 | target 'Runner' do
36 | use_frameworks!
37 | use_modular_headers!
38 |
39 | # Flutter Pod
40 |
41 | copied_flutter_dir = File.join(__dir__, 'Flutter')
42 | copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
43 | copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
44 | unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
45 | # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
46 | # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
47 | # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
48 |
49 | generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
50 | unless File.exist?(generated_xcode_build_settings_path)
51 | raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
52 | end
53 | generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
54 | cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
55 |
56 | unless File.exist?(copied_framework_path)
57 | FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
58 | end
59 | unless File.exist?(copied_podspec_path)
60 | FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
61 | end
62 | end
63 |
64 | # Keep pod path relative so it can be checked into Podfile.lock.
65 | pod 'Flutter', :path => 'Flutter'
66 |
67 | # Plugin Pods
68 |
69 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
70 | # referring to absolute paths on developers' machines.
71 | system('rm -rf .symlinks')
72 | system('mkdir -p .symlinks/plugins')
73 | plugin_pods = parse_KV_file('../.flutter-plugins')
74 | plugin_pods.each do |name, path|
75 | symlink = File.join('.symlinks', 'plugins', name)
76 | File.symlink(path, symlink)
77 | pod name, :path => File.join(symlink, 'ios')
78 | end
79 | end
80 |
81 | # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system.
82 | install! 'cocoapods', :disable_input_output_paths => true
83 |
84 | post_install do |installer|
85 | installer.pods_project.targets.each do |target|
86 | target.build_configurations.each do |config|
87 | config.build_settings['ENABLE_BITCODE'] = 'NO'
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/webapp/src/pages/Hero.js:
--------------------------------------------------------------------------------
1 |
2 | import React, { useState } from 'react'
3 | import { useHistory, useParams } from 'react-router-dom'
4 |
5 | import Typography from '@material-ui/core/Typography'
6 | import Tabs from '@material-ui/core/Tabs'
7 | import Tab from '@material-ui/core/Tab'
8 | import Container from '@material-ui/core/Container'
9 |
10 | import { useRecoilValue } from 'recoil'
11 |
12 | import { heroesSelector } from '../atoms/heroes'
13 |
14 | import HeroCard from '../components/HeroCard'
15 | import HeroGrid from '../components/HeroGrid'
16 |
17 | const imgStyle = {
18 | width : '100%',
19 | maxHeight : 240,
20 | objectFit : 'cover'
21 | }
22 |
23 | const titlebarStyle = {
24 | display:'flex',
25 | flexDirection : 'row',
26 | position : 'absolute',
27 | left : 0,
28 | bottom : 4,
29 | right : 0,
30 | height : 90,
31 | backgroundColor : 'rgba(0,0,0,0.5)',
32 | justifyContent : 'space-between',
33 | padding : 16,
34 | paddingBottom : 0,
35 | }
36 |
37 | const whiteBoldTextStyle = {
38 | fontWeight : 'bold',
39 | color : 'white'
40 | }
41 |
42 | const whiteTextStyle = {
43 | color : 'white'
44 | }
45 |
46 | function RecommendedHeroGrid({ heroes, onClick }){
47 | return (
48 |
49 | {heroes.map( hero => {
50 | return (
51 | onClick(hero.id)}/>
55 | )
56 | })}
57 |
58 | )
59 | }
60 |
61 | export default function Hero(){
62 | let { heroId } = useParams()
63 | const history = useHistory()
64 | const heroesList = useRecoilValue(heroesSelector)
65 | const [currentTab, setCurrentTab] = useState('bestHeroes')
66 | const hero = heroesList[heroId] || {}
67 |
68 | const bestHeroesIds = Object.keys(hero.bestHeroes || {})
69 | const worstHeroesIds = Object.keys(hero.worstHeroes || {})
70 |
71 | const goToHero = (id) => {
72 | setCurrentTab('bestHeroes')
73 | history.push(`/heroes/${id}`)
74 | }
75 |
76 | return (
77 |
78 |
79 |

80 |
81 |
82 |
{hero.name}
83 |
84 |
85 | Rank: {hero.rank}
86 |
87 | Win Rate: {hero.winRate}%
88 |
89 |
90 |
91 |
92 |
setCurrentTab(tab)} style={{ marginTop : -4}}
93 | indicatorColor="primary" variant="fullWidth" textColor="primary">
94 |
95 |
96 |
97 |
98 |
99 | { currentTab === 'bestHeroes' &&
100 | heroesList[id])} onClick={goToHero}/>}
101 | { currentTab === 'worstHeroes' &&
102 | heroesList[id])} onClick={goToHero}/>}
103 |
104 |
105 |
106 | )
107 | }
--------------------------------------------------------------------------------
/dialogflow/DotaAppAgent/intents/Default Welcome Intent_usersays_en.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "d0ea85eb-9214-420e-9b58-959fba81bc6f",
4 | "data": [
5 | {
6 | "text": "just going to say hi",
7 | "userDefined": false
8 | }
9 | ],
10 | "isTemplate": false,
11 | "count": 0,
12 | "updated": 0
13 | },
14 | {
15 | "id": "f3660fd6-786e-496d-8c02-16465e225eaa",
16 | "data": [
17 | {
18 | "text": "heya",
19 | "userDefined": false
20 | }
21 | ],
22 | "isTemplate": false,
23 | "count": 0,
24 | "updated": 0
25 | },
26 | {
27 | "id": "d056d3cf-1830-42fa-976a-92aa895ee876",
28 | "data": [
29 | {
30 | "text": "hello hi",
31 | "userDefined": false
32 | }
33 | ],
34 | "isTemplate": false,
35 | "count": 0,
36 | "updated": 0
37 | },
38 | {
39 | "id": "26893cc8-e7aa-4e62-ac9c-6fde99adbc76",
40 | "data": [
41 | {
42 | "text": "howdy",
43 | "userDefined": false
44 | }
45 | ],
46 | "isTemplate": false,
47 | "count": 0,
48 | "updated": 0
49 | },
50 | {
51 | "id": "4c320442-2d97-4820-a0ba-7d5e0d55ea30",
52 | "data": [
53 | {
54 | "text": "hey there",
55 | "userDefined": false
56 | }
57 | ],
58 | "isTemplate": false,
59 | "count": 0,
60 | "updated": 0
61 | },
62 | {
63 | "id": "d0f69537-811e-4700-bdb8-9f4cc4693127",
64 | "data": [
65 | {
66 | "text": "hi there",
67 | "userDefined": false
68 | }
69 | ],
70 | "isTemplate": false,
71 | "count": 1,
72 | "updated": 0
73 | },
74 | {
75 | "id": "f989e858-faea-439f-a814-fc69c52d8dc1",
76 | "data": [
77 | {
78 | "text": "greetings",
79 | "userDefined": false
80 | }
81 | ],
82 | "isTemplate": false,
83 | "count": 0,
84 | "updated": 0
85 | },
86 | {
87 | "id": "635fa236-f040-4d58-87af-6975e5214c73",
88 | "data": [
89 | {
90 | "text": "hey",
91 | "userDefined": false
92 | }
93 | ],
94 | "isTemplate": false,
95 | "count": 0,
96 | "updated": 0
97 | },
98 | {
99 | "id": "6d3c3735-6482-40a6-be71-c3f88b3f52c5",
100 | "data": [
101 | {
102 | "text": "long time no see",
103 | "userDefined": false
104 | }
105 | ],
106 | "isTemplate": false,
107 | "count": 0,
108 | "updated": 0
109 | },
110 | {
111 | "id": "1bc041e3-b8e7-45b2-adf3-c2a3cfb8183f",
112 | "data": [
113 | {
114 | "text": "hello",
115 | "userDefined": false
116 | }
117 | ],
118 | "isTemplate": false,
119 | "count": 0,
120 | "updated": 0
121 | },
122 | {
123 | "id": "4bef4ce7-4374-4269-a47e-aa2a9ec45016",
124 | "data": [
125 | {
126 | "text": "lovely day isn\u0027t it",
127 | "userDefined": false
128 | }
129 | ],
130 | "isTemplate": false,
131 | "count": 0,
132 | "updated": 0
133 | },
134 | {
135 | "id": "ac7ced29-7f37-4822-8dbe-866c9037c10c",
136 | "data": [
137 | {
138 | "text": "I greet you",
139 | "userDefined": false
140 | }
141 | ],
142 | "isTemplate": false,
143 | "count": 0,
144 | "updated": 0
145 | },
146 | {
147 | "id": "5c440daf-9318-4156-aafc-1f7577a6bd56",
148 | "data": [
149 | {
150 | "text": "hello again",
151 | "userDefined": false
152 | }
153 | ],
154 | "isTemplate": false,
155 | "count": 0,
156 | "updated": 0
157 | },
158 | {
159 | "id": "5c3b3ffe-e942-4841-bf46-67e4c0d4755c",
160 | "data": [
161 | {
162 | "text": "hi",
163 | "userDefined": false
164 | }
165 | ],
166 | "isTemplate": false,
167 | "count": 0,
168 | "updated": 0
169 | },
170 | {
171 | "id": "aa9c360b-ee82-4172-a6ed-cdbb3eeb8016",
172 | "data": [
173 | {
174 | "text": "hello there",
175 | "userDefined": false
176 | }
177 | ],
178 | "isTemplate": false,
179 | "count": 0,
180 | "updated": 0
181 | },
182 | {
183 | "id": "781eb347-b561-4278-8aa5-39c96f8f688b",
184 | "data": [
185 | {
186 | "text": "a good day",
187 | "userDefined": false
188 | }
189 | ],
190 | "isTemplate": false,
191 | "count": 0,
192 | "updated": 0
193 | }
194 | ]
--------------------------------------------------------------------------------
/go-api/controllers/hero.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "context"
5 | "os"
6 | "sort"
7 | "strconv"
8 | "strings"
9 |
10 | "com.aviebrantz.dota.api/model"
11 | "github.com/gofiber/fiber"
12 | )
13 |
14 | type HeroController struct {
15 | HeroRepository model.HeroRepository
16 | maxHeroesRecommended int
17 | }
18 |
19 | func NewHeroController(heroRepository model.HeroRepository) *HeroController {
20 | maxHeroesRecommended := 5
21 | if envMaxHeroesRecommended := os.Getenv("MAX_HEROES_RECOMMENDED"); envMaxHeroesRecommended != "" {
22 | v, err := strconv.Atoi(envMaxHeroesRecommended)
23 | if err == nil {
24 | maxHeroesRecommended = v
25 | }
26 | }
27 | return &HeroController{
28 | HeroRepository: heroRepository,
29 | maxHeroesRecommended: maxHeroesRecommended,
30 | }
31 | }
32 |
33 | func (hr *HeroController) GetHeroById(c *fiber.Ctx) {
34 | heroID := c.Params("heroId")
35 | ctx := context.Background()
36 | hero, err := hr.HeroRepository.FindById(ctx, heroID)
37 | if err != nil {
38 | c.Status(500).JSON(map[string]string{"message": "Internal error to fetch data"})
39 | return
40 | }
41 | if hero == nil {
42 | c.Status(404).JSON(map[string]string{"message": "Hero Not Found"})
43 | return
44 | }
45 | c.Status(200).JSON(hero)
46 | }
47 |
48 | func (hr *HeroController) GetHeroesRecommendations(c *fiber.Ctx) {
49 | enemies := c.Query("enemies")
50 | if enemies == "" {
51 | c.Status(400).JSON(map[string]string{"message": "Missing enemies query parameter"})
52 | return
53 | }
54 | enemiesIds := strings.Split(enemies, ",")
55 |
56 | if len(enemiesIds) > 5 {
57 | c.Status(400).JSON(map[string]string{"message": "Max number of enemies"})
58 | return
59 | }
60 |
61 | team := c.Query("team")
62 | teamIds := strings.Split(team, ",")
63 |
64 | if len(teamIds) > 5 {
65 | c.Status(400).JSON(map[string]string{"message": "Max number of team heroes"})
66 | return
67 | }
68 |
69 | heroesIds, err := hr.getRecommendations(enemiesIds, teamIds)
70 | if err != nil {
71 | c.Status(500).JSON(map[string]string{"message": err.Error()})
72 | return
73 | }
74 |
75 | finalHeroes, err := hr.HeroRepository.LoadHeroesList(heroesIds)
76 | if err != nil {
77 | c.Status(500).JSON(map[string]string{"message": err.Error()})
78 | return
79 | }
80 |
81 | finalHeroesMap := map[string]model.DotaHero{}
82 | for _, hero := range finalHeroes {
83 | hero.BestHeroes = nil
84 | hero.WorstHeroes = nil
85 | finalHeroesMap[hero.ID] = hero
86 | }
87 |
88 | c.Status(200).JSON(finalHeroesMap)
89 | }
90 |
91 | func (hc *HeroController) getRecommendations(enemiesIds, teamIds []string) ([]string, error) {
92 | teamMap := map[string]bool{}
93 | for _, teamID := range teamIds {
94 | teamMap[teamID] = true
95 | }
96 |
97 | enemyHeroes, err := hc.HeroRepository.LoadHeroesList(enemiesIds)
98 | if err != nil {
99 | return nil, err
100 | }
101 |
102 | intersections := make(map[string]model.DotaHeroVersus)
103 | enemyMap := map[string]bool{}
104 | for _, enemy := range enemyHeroes {
105 | enemyMap[enemy.ID] = true
106 | }
107 |
108 | for i, enemyA := range enemyHeroes {
109 | for j, enemyB := range enemyHeroes {
110 | if i == j {
111 | continue
112 | }
113 |
114 | for key := range enemyA.WorstHeroes {
115 | if _, ok := enemyMap[key]; ok {
116 | continue
117 | }
118 | if hero, ok := enemyB.WorstHeroes[key]; ok {
119 | _, isOnMyTeam := teamMap[key]
120 | if !isOnMyTeam {
121 | intersections[key] = hero
122 | }
123 | }
124 | }
125 | }
126 | }
127 |
128 | var heroesIds []string
129 | intersectCount := len(intersections)
130 | if intersectCount > 0 {
131 | for heroID := range intersections {
132 | heroesIds = append(heroesIds, heroID)
133 | }
134 | }
135 |
136 | if intersectCount < hc.maxHeroesRecommended {
137 | missingHeroes := hc.maxHeroesRecommended - intersectCount
138 | var allHeroes []model.DotaHeroVersus
139 | for _, enemy := range enemyHeroes {
140 | for _, hero := range enemy.WorstHeroes {
141 | _, isOnMyTeam := teamMap[hero.ID]
142 | _, isOnEnemyTeam := enemyMap[hero.ID]
143 | _, isOnIntersection := intersections[hero.ID]
144 | if !isOnMyTeam && !isOnIntersection && !isOnEnemyTeam {
145 | allHeroes = append(allHeroes, hero)
146 | }
147 | }
148 | }
149 | allHeroesLen := len(allHeroes)
150 | if allHeroesLen > 0 {
151 | sort.Sort(model.ByDotaHeroVersusAdvantage(allHeroes))
152 | cache := map[string]bool{}
153 | min := missingHeroes
154 | if allHeroesLen < missingHeroes {
155 | min = allHeroesLen
156 | }
157 | for _, hero := range allHeroes[0:min] {
158 | if _, ok := cache[hero.ID]; !ok {
159 | heroesIds = append(heroesIds, hero.ID)
160 | cache[hero.ID] = true
161 | }
162 | }
163 | }
164 | }
165 | return heroesIds, nil
166 | }
167 |
--------------------------------------------------------------------------------
/webapp/src/pages/TeamBuilder.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 |
3 | import { useRecoilValue } from 'recoil'
4 |
5 | import Fab from '@material-ui/core/Fab'
6 | import Button from '@material-ui/core/Button'
7 | import Container from '@material-ui/core/Container'
8 | import CircularProgress from '@material-ui/core/CircularProgress'
9 | import ResetIcon from '@material-ui/icons/Refresh'
10 | import DeleteIcon from '@material-ui/icons/Delete'
11 | import AddIcon from '@material-ui/icons/Add'
12 |
13 | import { useTheme } from '@material-ui/core/styles'
14 | import useMediaQuery from '@material-ui/core/useMediaQuery'
15 |
16 | import { heroesSelector } from '../atoms/heroes'
17 | import {
18 | useTeamBuilderActions,
19 | useRecommendedHeroes,
20 | availableHeroesIdsSelector,
21 | enemyHeroesSelector,
22 | teamHeroesSelector,
23 | } from '../atoms/builder'
24 |
25 | import HeroCard from '../components/HeroCard'
26 | import AddHeroCard from '../components/AddHeroCard'
27 | import HeroGrid from '../components/HeroGrid'
28 | import SelectHeroDialog from '../components/SelectHeroDialog'
29 |
30 | const fabStyle = {
31 | position : 'fixed',
32 | bottom : 80,
33 | right : 16,
34 | zIndex : 999
35 | }
36 |
37 | export default function TeamBuilder(){
38 | const {
39 | addEnemyHero,
40 | removeEnemyHero,
41 | addTeamHero,
42 | removeTeamHero,
43 | resetBuilder
44 | } = useTeamBuilderActions()
45 |
46 | const heroesList = useRecoilValue(heroesSelector)
47 | const availableHeroesIds = useRecoilValue(availableHeroesIdsSelector)
48 | const enemies = useRecoilValue(enemyHeroesSelector)
49 | const team = useRecoilValue(teamHeroesSelector)
50 |
51 | const [isRecommendedHeroesLoading, recommendedHeroes] = useRecommendedHeroes()
52 | const hasRecommendedHeroes = recommendedHeroes.length > 0
53 |
54 | const [isHeroDialogOpen, setHeroDialogOpen] = useState(false)
55 | const [dialogAction, setDialogAction] = useState('addEnemy')
56 |
57 | const actionMap = {
58 | 'addEnemy' : addEnemyHero,
59 | 'addTeam' : addTeamHero
60 | }
61 |
62 | const onHeroSelected = actionMap[dialogAction]
63 |
64 | const theme = useTheme()
65 | const isMobile = useMediaQuery(theme.breakpoints.down('xs'))
66 |
67 | return (
68 |
69 | {isMobile &&
72 |
73 | }
74 | {!isMobile && }
80 | Enemies
81 |
82 | {enemies.map( enemyId => {
83 | const hero = heroesList[enemyId]
84 | return (
85 | }
89 | onActionClick={removeEnemyHero}/>
90 | )
91 | })}
92 | {enemies.length < 5 && {
93 | setHeroDialogOpen(true)
94 | setDialogAction('addEnemy')
95 | }}/>}
96 |
97 |
98 | My Team
99 |
100 | {team.map( heroId => {
101 | const hero = heroesList[heroId]
102 | return (
103 | }
107 | onActionClick={removeTeamHero}/>
108 | )
109 | })}
110 | {team.length < 5 && {
111 | setHeroDialogOpen(true)
112 | setDialogAction('addTeam')
113 | }}/>}
114 |
115 |
116 | Recommended Heroes
117 | {!hasRecommendedHeroes && (
118 |
119 |
No Recommendations
120 |
121 | )}
122 | {isRecommendedHeroesLoading && enemies.length > 0 && (
123 |
124 |
125 |
126 | ) }
127 |
128 | {recommendedHeroes.map( heroId => {
129 | const hero = heroesList[heroId]
130 | return (
131 | }
135 | onActionClick={addTeamHero}/>
136 | )
137 | })}
138 |
139 |
140 | heroesList[id])}
142 | isOpen={isHeroDialogOpen}
143 | onHeroSelected={(id) => {
144 | onHeroSelected(id)
145 | setHeroDialogOpen(false)
146 | }}
147 | onClose={() => {
148 | setHeroDialogOpen(false)
149 | }} />
150 |
151 | )
152 | }
--------------------------------------------------------------------------------
/flutter_dota_app/lib/pages/hero_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:firebase_database/firebase_database.dart';
3 | import 'package:cached_network_image/cached_network_image.dart';
4 |
5 | import 'package:dota_app/model/hero.dart';
6 |
7 | class HeroPage extends StatefulWidget {
8 | HeroPage({Key key, this.hero}) : super(key: key);
9 |
10 | final DotaHero hero;
11 |
12 | @override
13 | _HeroPageState createState() => _HeroPageState();
14 | }
15 |
16 | class _HeroPageState extends State {
17 | DatabaseReference heroesRef;
18 | DatabaseReference heroRef;
19 | Map heroesCache = Map();
20 |
21 | @override
22 | void initState() {
23 | super.initState();
24 | heroesRef = FirebaseDatabase.instance.reference().child('/heroes');
25 |
26 | heroesRef.onValue.listen((data) {
27 | var heroesMap = Map.from(data.snapshot.value);
28 | heroesMap.keys.forEach((key) {
29 | var hero = DotaHero.fromMap(Map.from(heroesMap[key]));
30 | heroesCache[key] = hero;
31 | });
32 | });
33 | }
34 |
35 | List _buildHeroList(List heroes) {
36 | return heroes
37 | .map(
38 | (hero) => ListTile(
39 | title: Text(hero.name),
40 | subtitle: Text('${hero.matches} Matches'),
41 | trailing: Text('${hero.winRate}%'),
42 | ),
43 | )
44 | .toList();
45 | }
46 |
47 | @override
48 | Widget build(BuildContext context) {
49 | return Scaffold(
50 | appBar: AppBar(
51 | title: Text(this.widget.hero.name),
52 | ),
53 | body: Column(
54 | children: [
55 | Stack(
56 | children: [
57 | Container(
58 | height: 160,
59 | decoration: BoxDecoration(
60 | image: DecorationImage(
61 | image: CachedNetworkImageProvider(
62 | this.widget.hero.imageUrl,
63 | ),
64 | fit: BoxFit.cover,
65 | ),
66 | ),
67 | ),
68 | Container(
69 | height: 160,
70 | decoration: BoxDecoration(
71 | gradient: LinearGradient(
72 | begin: Alignment.topCenter,
73 | end: Alignment.bottomCenter,
74 | stops: [0.7, 1],
75 | colors: [Colors.black12, Colors.black]),
76 | ),
77 | ),
78 | Container(
79 | alignment: Alignment.bottomLeft,
80 | height: 160,
81 | padding: EdgeInsets.all(16),
82 | child: Text(
83 | this.widget.hero.name,
84 | style: Theme.of(context)
85 | .textTheme
86 | .display1
87 | .copyWith(color: Colors.white),
88 | ),
89 | ),
90 | ],
91 | ),
92 | Container(
93 | padding: EdgeInsets.all(16),
94 | color: Colors.black,
95 | child: Row(
96 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
97 | children: [
98 | HeroData('Rank ', '${this.widget.hero.rank}º'),
99 | HeroData('Win Rate ', '${this.widget.hero.winRate} %')
100 | ],
101 | ),
102 | ),
103 | Expanded(
104 | child: ListView(
105 | children: [
106 | Header('Best Heroes'),
107 | ..._buildHeroList(this.widget.hero.bestHeroes),
108 | Header('Worst Heroes'),
109 | ..._buildHeroList(this.widget.hero.worstHeroes)
110 | ],
111 | ),
112 | ),
113 | ],
114 | ),
115 | );
116 | }
117 | }
118 |
119 | class HeroData extends StatelessWidget {
120 | HeroData(this.label, this.value, {Key key}) : super(key: key);
121 |
122 | final String label;
123 | final String value;
124 |
125 | @override
126 | Widget build(BuildContext context) {
127 | var style =
128 | Theme.of(context).textTheme.subhead.copyWith(color: Colors.white);
129 | return Row(
130 | children: [
131 | Text(
132 | label,
133 | style: style.copyWith(fontWeight: FontWeight.bold),
134 | ),
135 | Text(
136 | value,
137 | style: style,
138 | ),
139 | ],
140 | );
141 | }
142 | }
143 |
144 | class Header extends StatelessWidget {
145 | Header(this.label, {Key key}) : super(key: key);
146 |
147 | final String label;
148 | @override
149 | Widget build(BuildContext context) {
150 | return Container(
151 | color: Colors.red.withOpacity(0.5),
152 | child: Row(
153 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
154 | mainAxisSize: MainAxisSize.max,
155 | children: [
156 | Text(this.label,
157 | style: TextStyle(
158 | fontSize: 20.0,
159 | fontWeight: FontWeight.bold,
160 | )),
161 | ],
162 | ),
163 | padding: EdgeInsets.all(10.0),
164 | );
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/webapp/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dota 2 App using Firebase and Google Cloud
2 |
3 | This is a pet project that born with the idea of having a nice subject to go through some Live Coding sessions. We are getting data from [DotaBuff](https://dotabuff.com) website to have more info about the heroes and latest matches.
4 |
5 | That info is going to be used on different frontends to build different features, some of them are :
6 |
7 | - Show which heroes are good/bad against a given hero.
8 | - Show a rank of best heroes
9 | - Build both match teams and get recommendations while heroes are being picked.
10 |
11 | Here you can see some screenshots of the mobile app, available on this repository, built with Flutter:
12 |
13 |
14 |
15 | Also we built a Voice integration that will allow users to access some of those same features from the app. The voice integration was built using Dialogflow and the agent data can be found on the `dialogflow` folder and the HTTP fullfilment is inside the `functions` folder.
16 |
17 | [Here you can a demo of the Voice integration (PT-BR)](https://twitter.com/alvaroviebrantz/status/1258213959787786246?s=20)
18 |
19 | The live codings session happens in Portuguese (PT-BR) and you can follow on my Youtube/Twitch channels.
20 |
21 | - [Alvaro Viebrantz on Youtube](https://youtube.com/alvaroviebrantz)
22 | - [Alvaro Viebrantz on Twitch](https://twitch.tv/alvaroviebrantz)
23 |
24 | ## Outstanding External Collaborators
25 |
26 | - [@omurilo](https://github.com/omurilo) Created a version of the mobile using React Native - [Link](https://github.com/omurilo/gcloud-react-native-dota-app)
27 |
28 | ## Table of Contents
29 |
30 | - [Getting Started](#getting-started)
31 | - [Node Setup](#node-setup)
32 | - [Firebase Setup](#firebase-setup)
33 | - [Google Cloud Setup](#google-cloud-tools-and-project)
34 | - [Flutter Setup](#google-cloud-tools-and-project)
35 | - [Building & Running](#building-&-running-the-project)
36 | - [Dialogflow Setup](#dialogflow-setup)
37 | - Project Structure
38 | - Cloud Function - `functions` folder
39 | - `scheduledFetchDotaBuffHeroes` : Scheduled PubSub handler that fetch all dota heroes and queue them up to be processed by `fetchDotaBuffHeroById`.
40 | - `fetchDotaBuffHeroById`: PubSub consumer that fetchs a given Dota hero data from DotaBuff and save data on Firebase.
41 | - `dialogflowFirebaseFulfillment`: Handle Dialogflow/Google Assistant Interaction
42 | - Go API - `go-api` folder
43 | - Hero Recommendation API.
44 | - We separated all the logic to recommend heroes to pick depending on the heroes that the enemy got and what you team already picked. We use two methods :
45 | - Intersection between all heroes that are good against the enemies heroes
46 | - Top heroes by sorted by advantage against all heroes picked by the enemies
47 | - API Always return at least 3 recommended heroes.
48 | - Flutter App - `flutter_dota_app` folder
49 | - Mobile App build with Flutter
50 | - Dialogflow Agent - `dialogflow` folder
51 | - All intents and entities used to build the voice integration.
52 |
53 | ## Getting Started
54 |
55 | ### Node Setup
56 |
57 | - Install the latest LTS version of [Node.js](https://nodejs.org/) (which includes npm). An easy way to do so is with `nvm`. (Mac and Linux: [here](https://github.com/creationix/nvm), Windows: [here](https://github.com/coreybutler/nvm-windows))
58 |
59 | ```shell
60 | nvm install --lts
61 | ```
62 |
63 | ### Firebase Setup
64 |
65 | - Install the [Firebase CLI](https://firebase.google.com/docs/cli) via `npm`. The following command enables the globally available `firebase` command:
66 |
67 | ```shell
68 | npm install -g firebase-tools
69 | ```
70 |
71 | - After installing the CLI, you must authenticate. Then you can confirm authentication by listing your Firebase projects. Sign into Firebase using your Google account by running the following command:
72 |
73 | ```shell
74 | firebase login
75 | ```
76 |
77 | - Test that the CLI is properly installed and accessing your account by listing your Firebase projects. Run the following command:
78 |
79 | ```shell
80 | firebase projects:list
81 | ```
82 |
83 | - Test that the CLI is properly installed and accessing your account by listing your Firebase projects. Run the following command:
84 |
85 | ```shell
86 | firebase projects:list
87 | ```
88 |
89 | ### Google Cloud Tools and Project
90 |
91 | - Install gcloud [CLI](https://cloud.google.com/sdk/install)
92 | - Authenticate with Google Cloud:
93 | - `gcloud auth login`
94 | - Create cloud project — choose your unique project name:
95 | - `gcloud projects create YOUR_PROJECT_NAME`
96 | - Set current project
97 | - `gcloud config set project YOUR_PROJECT_NAME`
98 | - Set current project
99 | - `firebase use YOUR_PROJECT_NAME`
100 |
101 | ### Flutter Setup
102 |
103 | - Follow the guide on their [website](https://flutter.dev/docs/get-started/install).
104 | - Run the following command to make sure it's all good.
105 |
106 | ```shell
107 | flutter doctor
108 | ```
109 |
110 | ## Building & Running the project
111 |
112 | - Make sure you have the latest packages (after you pull): `npm install`
113 | - Deploy all the function from the `functions` directory.
114 | - There are deploy scripts on the `package.json` file.
115 | - To run the app, run `flutter run` on the `flutter_dota_app` folder
116 |
117 | ## Dialogflow Setup
118 |
119 | 1. Create a [Dialogflow Agent](https://console.dialogflow.com/).
120 | 2. Go to **Settings** ⚙ > **Export and Import** > **Restore from zip** using the `dialogflow/DotaAppAgent.zip` in this directory.
121 | 3. `cd` to the `functions` directory
122 | 4. Run `firebase deploy --only functions:dialogflowFulfillment`
123 | 5. Back in Dialogflow Console > **Fulfullment** > **Enable** Webhook.
124 | - Paste the URL from the Firebase Console’s Trigger column under the **Functions > Dashboard** tab into the **URL** field > **Save**.
125 |
--------------------------------------------------------------------------------
/flutter_dota_app/ios/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - Firebase/Core (6.21.0):
3 | - Firebase/CoreOnly
4 | - FirebaseAnalytics (= 6.4.0)
5 | - Firebase/CoreOnly (6.21.0):
6 | - FirebaseCore (= 6.6.5)
7 | - Firebase/Database (6.21.0):
8 | - Firebase/CoreOnly
9 | - FirebaseDatabase (~> 6.1.4)
10 | - firebase_core (0.0.1):
11 | - Firebase/Core
12 | - Flutter
13 | - firebase_core_web (0.1.0):
14 | - Flutter
15 | - firebase_database (0.0.1):
16 | - Firebase/Database
17 | - Flutter
18 | - FirebaseAnalytics (6.4.0):
19 | - FirebaseCore (~> 6.6)
20 | - FirebaseInstallations (~> 1.1)
21 | - GoogleAppMeasurement (= 6.4.0)
22 | - GoogleUtilities/AppDelegateSwizzler (~> 6.0)
23 | - GoogleUtilities/MethodSwizzler (~> 6.0)
24 | - GoogleUtilities/Network (~> 6.0)
25 | - "GoogleUtilities/NSData+zlib (~> 6.0)"
26 | - nanopb (= 0.3.9011)
27 | - FirebaseAuthInterop (1.1.0)
28 | - FirebaseCore (6.6.5):
29 | - FirebaseCoreDiagnostics (~> 1.2)
30 | - FirebaseCoreDiagnosticsInterop (~> 1.2)
31 | - GoogleUtilities/Environment (~> 6.5)
32 | - GoogleUtilities/Logger (~> 6.5)
33 | - FirebaseCoreDiagnostics (1.2.2):
34 | - FirebaseCoreDiagnosticsInterop (~> 1.2)
35 | - GoogleDataTransportCCTSupport (~> 2.0)
36 | - GoogleUtilities/Environment (~> 6.5)
37 | - GoogleUtilities/Logger (~> 6.5)
38 | - nanopb (~> 0.3.901)
39 | - FirebaseCoreDiagnosticsInterop (1.2.0)
40 | - FirebaseDatabase (6.1.4):
41 | - FirebaseAuthInterop (~> 1.0)
42 | - FirebaseCore (~> 6.0)
43 | - leveldb-library (~> 1.22)
44 | - FirebaseInstallations (1.1.1):
45 | - FirebaseCore (~> 6.6)
46 | - GoogleUtilities/UserDefaults (~> 6.5)
47 | - PromisesObjC (~> 1.2)
48 | - Flutter (1.0.0)
49 | - FMDB (2.7.5):
50 | - FMDB/standard (= 2.7.5)
51 | - FMDB/standard (2.7.5)
52 | - GoogleAppMeasurement (6.4.0):
53 | - GoogleUtilities/AppDelegateSwizzler (~> 6.0)
54 | - GoogleUtilities/MethodSwizzler (~> 6.0)
55 | - GoogleUtilities/Network (~> 6.0)
56 | - "GoogleUtilities/NSData+zlib (~> 6.0)"
57 | - nanopb (= 0.3.9011)
58 | - GoogleDataTransport (5.1.0)
59 | - GoogleDataTransportCCTSupport (2.0.1):
60 | - GoogleDataTransport (~> 5.1)
61 | - nanopb (~> 0.3.901)
62 | - GoogleUtilities/AppDelegateSwizzler (6.5.2):
63 | - GoogleUtilities/Environment
64 | - GoogleUtilities/Logger
65 | - GoogleUtilities/Network
66 | - GoogleUtilities/Environment (6.5.2)
67 | - GoogleUtilities/Logger (6.5.2):
68 | - GoogleUtilities/Environment
69 | - GoogleUtilities/MethodSwizzler (6.5.2):
70 | - GoogleUtilities/Logger
71 | - GoogleUtilities/Network (6.5.2):
72 | - GoogleUtilities/Logger
73 | - "GoogleUtilities/NSData+zlib"
74 | - GoogleUtilities/Reachability
75 | - "GoogleUtilities/NSData+zlib (6.5.2)"
76 | - GoogleUtilities/Reachability (6.5.2):
77 | - GoogleUtilities/Logger
78 | - GoogleUtilities/UserDefaults (6.5.2):
79 | - GoogleUtilities/Logger
80 | - leveldb-library (1.22)
81 | - nanopb (0.3.9011):
82 | - nanopb/decode (= 0.3.9011)
83 | - nanopb/encode (= 0.3.9011)
84 | - nanopb/decode (0.3.9011)
85 | - nanopb/encode (0.3.9011)
86 | - path_provider (0.0.1):
87 | - Flutter
88 | - path_provider_macos (0.0.1):
89 | - Flutter
90 | - PromisesObjC (1.2.8)
91 | - sqflite (0.0.1):
92 | - Flutter
93 | - FMDB (~> 2.7.2)
94 |
95 | DEPENDENCIES:
96 | - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
97 | - firebase_core_web (from `.symlinks/plugins/firebase_core_web/ios`)
98 | - firebase_database (from `.symlinks/plugins/firebase_database/ios`)
99 | - Flutter (from `Flutter`)
100 | - path_provider (from `.symlinks/plugins/path_provider/ios`)
101 | - path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`)
102 | - sqflite (from `.symlinks/plugins/sqflite/ios`)
103 |
104 | SPEC REPOS:
105 | trunk:
106 | - Firebase
107 | - FirebaseAnalytics
108 | - FirebaseAuthInterop
109 | - FirebaseCore
110 | - FirebaseCoreDiagnostics
111 | - FirebaseCoreDiagnosticsInterop
112 | - FirebaseDatabase
113 | - FirebaseInstallations
114 | - FMDB
115 | - GoogleAppMeasurement
116 | - GoogleDataTransport
117 | - GoogleDataTransportCCTSupport
118 | - GoogleUtilities
119 | - leveldb-library
120 | - nanopb
121 | - PromisesObjC
122 |
123 | EXTERNAL SOURCES:
124 | firebase_core:
125 | :path: ".symlinks/plugins/firebase_core/ios"
126 | firebase_core_web:
127 | :path: ".symlinks/plugins/firebase_core_web/ios"
128 | firebase_database:
129 | :path: ".symlinks/plugins/firebase_database/ios"
130 | Flutter:
131 | :path: Flutter
132 | path_provider:
133 | :path: ".symlinks/plugins/path_provider/ios"
134 | path_provider_macos:
135 | :path: ".symlinks/plugins/path_provider_macos/ios"
136 | sqflite:
137 | :path: ".symlinks/plugins/sqflite/ios"
138 |
139 | SPEC CHECKSUMS:
140 | Firebase: f378c80340dd41c0ad0914af740c021eb282a04b
141 | firebase_core: 0d8be0e0d14c4902953aeb5ac5d7316d1fe4b978
142 | firebase_core_web: d501d8b946b60c8af265428ce483b0fff5ad52d1
143 | firebase_database: ba12319259e324c8e52e5e4efc8c6f3d33da1ee3
144 | FirebaseAnalytics: a1a0b3327ceb5cd5b4bacffdb293f6c909aa087d
145 | FirebaseAuthInterop: a0f37ae05833af156e72028f648d313f7e7592e9
146 | FirebaseCore: 9f495d3afacb7b558711e6218ebb14b1c51b5802
147 | FirebaseCoreDiagnostics: e9b4cd8ba60dee0f2d13347332e4b7898cca5b61
148 | FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850
149 | FirebaseDatabase: 0144e0706a4761f1b0e8679572eba8095ddb59be
150 | FirebaseInstallations: acb3216eb9784d3b1d2d2d635ff74fa892cc0c44
151 | Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
152 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
153 | GoogleAppMeasurement: 6e68a94d0eaeb1d73ef6b0ed4f7334e29d63ae29
154 | GoogleDataTransport: b29a21d813e906014ca16c00897827e40e4a24ab
155 | GoogleDataTransportCCTSupport: 6f15a89b0ca35d6fa523e1f752ef818588885988
156 | GoogleUtilities: ad0f3b691c67909d03a3327cc205222ab8f42e0e
157 | leveldb-library: 55d93ee664b4007aac644a782d11da33fba316f7
158 | nanopb: 18003b5e52dab79db540fe93fe9579f399bd1ccd
159 | path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
160 | path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0
161 | PromisesObjC: c119f3cd559f50b7ae681fa59dc1acd19173b7e6
162 | sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0
163 |
164 | PODFILE CHECKSUM: 1b66dae606f75376c5f2135a8290850eeb09ae83
165 |
166 | COCOAPODS: 1.8.4
167 |
--------------------------------------------------------------------------------
/go-api/controllers/hero_test.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "context"
5 | "io/ioutil"
6 | "net/http"
7 | "strings"
8 | "testing"
9 |
10 | "com.aviebrantz.dota.api/model"
11 | "github.com/gofiber/fiber"
12 | )
13 |
14 | type FakeHeroRepository struct {
15 | database map[string]*model.DotaHero
16 | }
17 |
18 | func (fhr *FakeHeroRepository) FindById(ctx context.Context, id string) (*model.DotaHero, error) {
19 | hero := fhr.database[id]
20 | return hero, nil
21 | }
22 |
23 | func (fhr *FakeHeroRepository) LoadHeroesList(ids []string) ([]model.DotaHero, error) {
24 | var heroes []model.DotaHero
25 | for _, id := range ids {
26 | hero := fhr.database[id]
27 | heroes = append(heroes, *hero)
28 | }
29 | return heroes, nil
30 | }
31 |
32 | func TestGetRecommendationIntersection(t *testing.T) {
33 | heroRepository := &FakeHeroRepository{
34 | database: map[string]*model.DotaHero{
35 | "h1": {
36 | ID: "h1",
37 | WorstHeroes: map[string]model.DotaHeroVersus{
38 | "h4": {
39 | ID: "h4",
40 | },
41 | "h2": {
42 | ID: "h2",
43 | },
44 | },
45 | },
46 | "h2": {
47 | ID: "h2",
48 | WorstHeroes: map[string]model.DotaHeroVersus{
49 | "h4": {
50 | ID: "h4",
51 | },
52 | "h5": {
53 | ID: "h5",
54 | },
55 | },
56 | },
57 | "h3": {
58 | ID: "h3",
59 | WorstHeroes: map[string]model.DotaHeroVersus{
60 | "h5": {
61 | ID: "h5",
62 | },
63 | },
64 | },
65 | },
66 | }
67 | controller := NewHeroController(heroRepository)
68 |
69 | enemiesIds := []string{"h1", "h2"}
70 | teamIds := []string{}
71 |
72 | heroesIds, _ := controller.getRecommendations(enemiesIds, teamIds)
73 | expected := "h4,h5"
74 | joinedHeroesIds := strings.Join(heroesIds, ",")
75 |
76 | if joinedHeroesIds != expected {
77 | t.Errorf("getRecommendation(h1,h2) = %s; want %s", joinedHeroesIds, expected)
78 | }
79 | }
80 |
81 | func TestGetRecommendationIntersectionFillTopHero(t *testing.T) {
82 | heroRepository := &FakeHeroRepository{
83 | database: map[string]*model.DotaHero{
84 | "h1": {
85 | ID: "h1",
86 | WorstHeroes: map[string]model.DotaHeroVersus{
87 | "h4": {
88 | ID: "h4",
89 | },
90 | },
91 | },
92 | "h2": {
93 | ID: "h2",
94 | WorstHeroes: map[string]model.DotaHeroVersus{
95 | "h4": {
96 | ID: "h4",
97 | },
98 | "h5": {
99 | ID: "h5",
100 | Advantage: 10,
101 | },
102 | },
103 | },
104 | "h3": {
105 | ID: "h3",
106 | WorstHeroes: map[string]model.DotaHeroVersus{
107 | "h6": {
108 | ID: "h6",
109 | Advantage: 20,
110 | },
111 | "h7": {
112 | ID: "h7",
113 | Advantage: 30,
114 | },
115 | },
116 | },
117 | },
118 | }
119 | controller := NewHeroController(heroRepository)
120 |
121 | enemiesIds := []string{"h1", "h2", "h3"}
122 | teamIds := []string{"h7"}
123 |
124 | heroesIds, _ := controller.getRecommendations(enemiesIds, teamIds)
125 | expected := "h4,h6,h5"
126 | joinedHeroesIds := strings.Join(heroesIds, ",")
127 |
128 | if joinedHeroesIds != expected {
129 | t.Errorf("getRecommendation((h1,h2,h3),(h7)) = %s; want %s", joinedHeroesIds, expected)
130 | }
131 | }
132 |
133 | func TestGetHeroBydId(t *testing.T) {
134 | heroRepository := &FakeHeroRepository{
135 | database: map[string]*model.DotaHero{
136 | "h1": {
137 | ID: "h1",
138 | ImageURL: "/img",
139 | Name: "Hero 1",
140 | Rank: 1,
141 | WinRate: 50.1,
142 | },
143 | },
144 | }
145 |
146 | tests := []struct {
147 | description string
148 | route string
149 | expectedError bool
150 | expectedCode int
151 | expectedBody string
152 | }{
153 | {
154 | description: "Find Hero 1",
155 | route: "/h1",
156 | expectedCode: 200,
157 | expectedBody: `{"id":"h1","imageUrl":"/img","name":"Hero 1","rank":1,"winRate":50.1}`,
158 | },
159 | {
160 | description: "Non Existing Hero",
161 | route: "/not-hero",
162 | expectedCode: 404,
163 | expectedBody: `{"message":"Hero Not Found"}`,
164 | },
165 | }
166 |
167 | controller := NewHeroController(heroRepository)
168 | app := fiber.New()
169 | app.Get("/:heroId", controller.GetHeroById)
170 | // Iterate through test single test cases
171 | for _, test := range tests {
172 |
173 | req, _ := http.NewRequest(
174 | "GET",
175 | test.route,
176 | nil,
177 | )
178 |
179 | res, err := app.Test(req, -1)
180 | if err != nil {
181 | t.Errorf("error sending test request: %s", err.Error())
182 | }
183 |
184 | if res.StatusCode != test.expectedCode {
185 | t.Errorf("expected status code %d, receivd %d", test.expectedCode, res.StatusCode)
186 | }
187 |
188 | body, err := ioutil.ReadAll(res.Body)
189 | if err != nil {
190 | t.Errorf("error getting request body: %s", err.Error())
191 | }
192 |
193 | if string(body) != test.expectedBody {
194 | t.Errorf("GetHeroById(%s) = %s; want %s", test.description, string(body), test.expectedBody)
195 | }
196 | }
197 | }
198 |
199 | func TestGetHeroRecommendation(t *testing.T) {
200 | heroRepository := &FakeHeroRepository{
201 | database: map[string]*model.DotaHero{
202 | "h1": {
203 | ID: "h1",
204 | WorstHeroes: map[string]model.DotaHeroVersus{
205 | "h3": {
206 | ID: "h3",
207 | },
208 | },
209 | },
210 | "h3": {
211 | ID: "h3",
212 | ImageURL: "/img",
213 | Name: "Hero 3",
214 | Rank: 3,
215 | WinRate: 50.1,
216 | },
217 | },
218 | }
219 |
220 | tests := []struct {
221 | description string
222 | route string
223 | expectedError bool
224 | expectedCode int
225 | expectedBody string
226 | }{
227 | {
228 | description: "Missing enemies parameter",
229 | route: "/rec",
230 | expectedCode: 400,
231 | expectedBody: `{"message":"Missing enemies query parameter"}`,
232 | },
233 | {
234 | description: "Max enemies",
235 | route: "/rec?enemies=h1,h2,h3,h4,h5,h6",
236 | expectedCode: 400,
237 | expectedBody: `{"message":"Max number of enemies"}`,
238 | },
239 | {
240 | description: "Max own team heroes",
241 | route: "/rec?enemies=h7&team=h1,h2,h3,h4,h5,h6",
242 | expectedCode: 400,
243 | expectedBody: `{"message":"Max number of team heroes"}`,
244 | },
245 | {
246 | description: "Return hero recommendation",
247 | route: "/rec?enemies=h1",
248 | expectedCode: 200,
249 | expectedBody: `{"h3":{"id":"h3","imageUrl":"/img","name":"Hero 3","rank":3,"winRate":50.1}}`,
250 | },
251 | }
252 |
253 | controller := NewHeroController(heroRepository)
254 | app := fiber.New()
255 | app.Get("/rec", controller.GetHeroesRecommendations)
256 | // Iterate through test single test cases
257 | for _, test := range tests {
258 |
259 | req, _ := http.NewRequest(
260 | "GET",
261 | test.route,
262 | nil,
263 | )
264 |
265 | res, err := app.Test(req, -1)
266 | if err != nil {
267 | t.Errorf("error sending test request: %s", err.Error())
268 | }
269 |
270 | if res.StatusCode != test.expectedCode {
271 | t.Errorf("expected status code %d, receivd %d", test.expectedCode, res.StatusCode)
272 | }
273 |
274 | body, err := ioutil.ReadAll(res.Body)
275 | if err != nil {
276 | t.Errorf("error getting request body: %s", err.Error())
277 | }
278 |
279 | if string(body) != test.expectedBody {
280 | t.Errorf("GetHeroesRecommendations(%s) = %s; want %s", test.description, string(body), test.expectedBody)
281 | }
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/flutter_dota_app/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://dart.dev/tools/pub/glossary#lockfile
3 | packages:
4 | archive:
5 | dependency: transitive
6 | description:
7 | name: archive
8 | url: "https://pub.dartlang.org"
9 | source: hosted
10 | version: "2.0.11"
11 | args:
12 | dependency: transitive
13 | description:
14 | name: args
15 | url: "https://pub.dartlang.org"
16 | source: hosted
17 | version: "1.5.2"
18 | async:
19 | dependency: transitive
20 | description:
21 | name: async
22 | url: "https://pub.dartlang.org"
23 | source: hosted
24 | version: "2.4.0"
25 | boolean_selector:
26 | dependency: transitive
27 | description:
28 | name: boolean_selector
29 | url: "https://pub.dartlang.org"
30 | source: hosted
31 | version: "1.0.5"
32 | cached_network_image:
33 | dependency: "direct main"
34 | description:
35 | name: cached_network_image
36 | url: "https://pub.dartlang.org"
37 | source: hosted
38 | version: "2.1.0+1"
39 | charcode:
40 | dependency: transitive
41 | description:
42 | name: charcode
43 | url: "https://pub.dartlang.org"
44 | source: hosted
45 | version: "1.1.2"
46 | clock:
47 | dependency: transitive
48 | description:
49 | name: clock
50 | url: "https://pub.dartlang.org"
51 | source: hosted
52 | version: "1.0.1"
53 | collection:
54 | dependency: transitive
55 | description:
56 | name: collection
57 | url: "https://pub.dartlang.org"
58 | source: hosted
59 | version: "1.14.11"
60 | convert:
61 | dependency: transitive
62 | description:
63 | name: convert
64 | url: "https://pub.dartlang.org"
65 | source: hosted
66 | version: "2.1.1"
67 | crypto:
68 | dependency: transitive
69 | description:
70 | name: crypto
71 | url: "https://pub.dartlang.org"
72 | source: hosted
73 | version: "2.1.3"
74 | cupertino_icons:
75 | dependency: "direct main"
76 | description:
77 | name: cupertino_icons
78 | url: "https://pub.dartlang.org"
79 | source: hosted
80 | version: "0.1.3"
81 | file:
82 | dependency: transitive
83 | description:
84 | name: file
85 | url: "https://pub.dartlang.org"
86 | source: hosted
87 | version: "5.1.0"
88 | firebase:
89 | dependency: transitive
90 | description:
91 | name: firebase
92 | url: "https://pub.dartlang.org"
93 | source: hosted
94 | version: "7.3.0"
95 | firebase_core:
96 | dependency: "direct main"
97 | description:
98 | name: firebase_core
99 | url: "https://pub.dartlang.org"
100 | source: hosted
101 | version: "0.4.4+3"
102 | firebase_core_platform_interface:
103 | dependency: transitive
104 | description:
105 | name: firebase_core_platform_interface
106 | url: "https://pub.dartlang.org"
107 | source: hosted
108 | version: "1.0.4"
109 | firebase_core_web:
110 | dependency: transitive
111 | description:
112 | name: firebase_core_web
113 | url: "https://pub.dartlang.org"
114 | source: hosted
115 | version: "0.1.1+2"
116 | firebase_database:
117 | dependency: "direct main"
118 | description:
119 | name: firebase_database
120 | url: "https://pub.dartlang.org"
121 | source: hosted
122 | version: "3.1.5"
123 | flutter:
124 | dependency: "direct main"
125 | description: flutter
126 | source: sdk
127 | version: "0.0.0"
128 | flutter_cache_manager:
129 | dependency: transitive
130 | description:
131 | name: flutter_cache_manager
132 | url: "https://pub.dartlang.org"
133 | source: hosted
134 | version: "1.2.2"
135 | flutter_test:
136 | dependency: "direct dev"
137 | description: flutter
138 | source: sdk
139 | version: "0.0.0"
140 | flutter_web_plugins:
141 | dependency: transitive
142 | description: flutter
143 | source: sdk
144 | version: "0.0.0"
145 | http:
146 | dependency: transitive
147 | description:
148 | name: http
149 | url: "https://pub.dartlang.org"
150 | source: hosted
151 | version: "0.12.1"
152 | http_parser:
153 | dependency: transitive
154 | description:
155 | name: http_parser
156 | url: "https://pub.dartlang.org"
157 | source: hosted
158 | version: "3.1.4"
159 | image:
160 | dependency: transitive
161 | description:
162 | name: image
163 | url: "https://pub.dartlang.org"
164 | source: hosted
165 | version: "2.1.4"
166 | intl:
167 | dependency: transitive
168 | description:
169 | name: intl
170 | url: "https://pub.dartlang.org"
171 | source: hosted
172 | version: "0.16.1"
173 | js:
174 | dependency: transitive
175 | description:
176 | name: js
177 | url: "https://pub.dartlang.org"
178 | source: hosted
179 | version: "0.6.1+1"
180 | matcher:
181 | dependency: transitive
182 | description:
183 | name: matcher
184 | url: "https://pub.dartlang.org"
185 | source: hosted
186 | version: "0.12.6"
187 | meta:
188 | dependency: transitive
189 | description:
190 | name: meta
191 | url: "https://pub.dartlang.org"
192 | source: hosted
193 | version: "1.1.8"
194 | path:
195 | dependency: transitive
196 | description:
197 | name: path
198 | url: "https://pub.dartlang.org"
199 | source: hosted
200 | version: "1.6.4"
201 | path_provider:
202 | dependency: transitive
203 | description:
204 | name: path_provider
205 | url: "https://pub.dartlang.org"
206 | source: hosted
207 | version: "1.6.7"
208 | path_provider_macos:
209 | dependency: transitive
210 | description:
211 | name: path_provider_macos
212 | url: "https://pub.dartlang.org"
213 | source: hosted
214 | version: "0.0.4+1"
215 | path_provider_platform_interface:
216 | dependency: transitive
217 | description:
218 | name: path_provider_platform_interface
219 | url: "https://pub.dartlang.org"
220 | source: hosted
221 | version: "1.0.1"
222 | pedantic:
223 | dependency: transitive
224 | description:
225 | name: pedantic
226 | url: "https://pub.dartlang.org"
227 | source: hosted
228 | version: "1.8.0+1"
229 | petitparser:
230 | dependency: transitive
231 | description:
232 | name: petitparser
233 | url: "https://pub.dartlang.org"
234 | source: hosted
235 | version: "2.4.0"
236 | platform:
237 | dependency: transitive
238 | description:
239 | name: platform
240 | url: "https://pub.dartlang.org"
241 | source: hosted
242 | version: "2.2.1"
243 | plugin_platform_interface:
244 | dependency: transitive
245 | description:
246 | name: plugin_platform_interface
247 | url: "https://pub.dartlang.org"
248 | source: hosted
249 | version: "1.0.2"
250 | quiver:
251 | dependency: transitive
252 | description:
253 | name: quiver
254 | url: "https://pub.dartlang.org"
255 | source: hosted
256 | version: "2.0.5"
257 | rxdart:
258 | dependency: transitive
259 | description:
260 | name: rxdart
261 | url: "https://pub.dartlang.org"
262 | source: hosted
263 | version: "0.24.0"
264 | sky_engine:
265 | dependency: transitive
266 | description: flutter
267 | source: sdk
268 | version: "0.0.99"
269 | source_span:
270 | dependency: transitive
271 | description:
272 | name: source_span
273 | url: "https://pub.dartlang.org"
274 | source: hosted
275 | version: "1.5.5"
276 | sqflite:
277 | dependency: transitive
278 | description:
279 | name: sqflite
280 | url: "https://pub.dartlang.org"
281 | source: hosted
282 | version: "1.3.0"
283 | sqflite_common:
284 | dependency: transitive
285 | description:
286 | name: sqflite_common
287 | url: "https://pub.dartlang.org"
288 | source: hosted
289 | version: "1.0.0+1"
290 | stack_trace:
291 | dependency: transitive
292 | description:
293 | name: stack_trace
294 | url: "https://pub.dartlang.org"
295 | source: hosted
296 | version: "1.9.3"
297 | stream_channel:
298 | dependency: transitive
299 | description:
300 | name: stream_channel
301 | url: "https://pub.dartlang.org"
302 | source: hosted
303 | version: "2.0.0"
304 | string_scanner:
305 | dependency: transitive
306 | description:
307 | name: string_scanner
308 | url: "https://pub.dartlang.org"
309 | source: hosted
310 | version: "1.0.5"
311 | synchronized:
312 | dependency: transitive
313 | description:
314 | name: synchronized
315 | url: "https://pub.dartlang.org"
316 | source: hosted
317 | version: "2.2.0"
318 | term_glyph:
319 | dependency: transitive
320 | description:
321 | name: term_glyph
322 | url: "https://pub.dartlang.org"
323 | source: hosted
324 | version: "1.1.0"
325 | test_api:
326 | dependency: transitive
327 | description:
328 | name: test_api
329 | url: "https://pub.dartlang.org"
330 | source: hosted
331 | version: "0.2.11"
332 | typed_data:
333 | dependency: transitive
334 | description:
335 | name: typed_data
336 | url: "https://pub.dartlang.org"
337 | source: hosted
338 | version: "1.1.6"
339 | uuid:
340 | dependency: transitive
341 | description:
342 | name: uuid
343 | url: "https://pub.dartlang.org"
344 | source: hosted
345 | version: "2.0.4"
346 | vector_math:
347 | dependency: transitive
348 | description:
349 | name: vector_math
350 | url: "https://pub.dartlang.org"
351 | source: hosted
352 | version: "2.0.8"
353 | xml:
354 | dependency: transitive
355 | description:
356 | name: xml
357 | url: "https://pub.dartlang.org"
358 | source: hosted
359 | version: "3.5.0"
360 | sdks:
361 | dart: ">=2.7.0 <3.0.0"
362 | flutter: ">=1.12.13+hotfix.5 <2.0.0"
363 |
--------------------------------------------------------------------------------
/functions/src/index.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions');
2 | const fetch = require('node-fetch');
3 | const cheerio = require('cheerio');
4 | const firebaseAdmin = require('firebase-admin');
5 | const { PubSub } = require('@google-cloud/pubsub')
6 | const { WebhookClient } = require('dialogflow-fulfillment')
7 |
8 | firebaseAdmin.initializeApp()
9 |
10 | const db = firebaseAdmin.database()
11 |
12 | const dotaBuffBaseUrl = 'https://www.dotabuff.com'
13 | const dotaBuffHeroesUrl = `${dotaBuffBaseUrl}/heroes`
14 | const dotaBuffHeroesUrlBy = (id) => `${dotaBuffBaseUrl}/heroes/${id}`
15 |
16 | const toSlug = (text) => {
17 | const lower = text.toLowerCase()
18 | const cleanText = lower.replace("'", '')
19 | return cleanText.split(' ').join('-')
20 | }
21 |
22 | async function fetchHeroById(id) {
23 | const data = await fetch(dotaBuffHeroesUrlBy(id))
24 | if (!data.ok) {
25 | throw new Error('Hero not found')
26 | }
27 |
28 | const html = await data.text()
29 | const $ = cheerio.load(html)
30 |
31 | const hero = {
32 | id
33 | }
34 |
35 | hero.winRate = parseFloat($('.header-content-secondary span').first().text().trim())
36 | hero.rank = parseFloat($('.header-content-secondary dd').first().text().trim())
37 |
38 | $('section').each((index, el) => {
39 | const sectionName = $(el).find('header').text().trim()
40 | if (sectionName.includes('Best Versus')) {
41 | hero.bestHeroes = hero.bestHeroes || {}
42 | $(el).find('tbody tr').each((j, trEl) => {
43 | const arr = $(trEl).children('td').map((i, td) => $(td).text().trim()).toArray()
44 | const name = arr[1]
45 | const id = toSlug(name)
46 | hero.bestHeroes[id] = {
47 | id,
48 | name,
49 | advantage: parseFloat(arr[2]),
50 | winRate: parseFloat(arr[3]),
51 | matches: parseInt(arr[4].replace(',',''),10)
52 | }
53 | })
54 | }
55 |
56 | if (sectionName.includes('Worst Versus')) {
57 | hero.worstHeroes = hero.worstHeroes || {}
58 | $(el).find('tbody tr').each((j, trEl) => {
59 | const arr = $(trEl).children('td').map((i, td) => $(td).text().trim()).toArray()
60 | const name = arr[1]
61 | const id = toSlug(name)
62 | hero.worstHeroes[id] = {
63 | id,
64 | name,
65 | advantage: parseFloat(arr[2]),
66 | winRate: parseFloat(arr[3]),
67 | matches: parseInt(arr[4].replace(',',''),10)
68 | }
69 | })
70 | }
71 | })
72 |
73 | return hero
74 | }
75 |
76 | const fetchDotaHeroTopic = 'fetch-dota-hero-topic'
77 | exports.fetchDotaBuffHeroById = functions.pubsub.topic(fetchDotaHeroTopic)
78 | .onPublish(async (msg) => {
79 | try {
80 | const { id } = msg.json
81 | const hero = await fetchHeroById(id)
82 | const heroRef = db.ref('/heroes').child(id)
83 | await heroRef.update(hero)
84 | } catch (err) {
85 | console.error(err)
86 | }
87 | })
88 |
89 | exports.scheduledFetchDotaBuffHeroes = functions.pubsub.schedule('0 3 * * *').onRun( async (context) => {
90 |
91 | const data = await fetch(dotaBuffHeroesUrl)
92 | const html = await data.text()
93 |
94 | const $ = cheerio.load(html)
95 |
96 | const heroes = $('.hero-grid a').map((_, el) => {
97 | const name = $(el).text().trim()
98 | const id = toSlug(name)
99 | let imageUrl = $(el).find('.hero').css('background')
100 | imageUrl = imageUrl.replace('url(', '')
101 | imageUrl = imageUrl.replace(')', '')
102 | imageUrl = dotaBuffBaseUrl + imageUrl
103 | return {
104 | id,
105 | name,
106 | imageUrl
107 | }
108 | }).toArray()
109 |
110 | const heroesMap = {}
111 | heroes.forEach(hero => {
112 | Object.keys(hero).forEach(attr => {
113 | heroesMap[`${hero.id}/${attr}`] = hero[attr]
114 | })
115 | })
116 |
117 | const pubSub = new PubSub({
118 | projectId : process.env.GCLOUD_PROJECT
119 | })
120 |
121 | // console.log('Map de atualizacao', heroesMap)
122 |
123 | const publishPromises =heroes.map(hero => {
124 | return pubSub.topic(fetchDotaHeroTopic).publishJSON({ id : hero.id });
125 | })
126 |
127 | const heroesRef = db.ref('/heroes')
128 | await heroesRef.update(heroesMap)
129 | await Promise.all(publishPromises)
130 | })
131 |
132 | function joinOr(names, joiner= 'ou'){
133 | if(names.length === 0){
134 | return 0
135 | }
136 |
137 | if(names.length === 1){
138 | return names[0]
139 | }
140 |
141 | if(names.length === 2){
142 | return names.join(` ${joiner} `)
143 | }
144 |
145 | if(names.length > 2){
146 | const firstNames = names.slice(0,-1)
147 | const lastName = names[names.length - 1]
148 | return firstNames.join(', ') + ` ${joiner} ` + lastName
149 | }
150 | }
151 |
152 | function pickOne(list){
153 | const max = list.length
154 | const pick = Math.floor(Math.random() * max)
155 | return list[pick]
156 | }
157 |
158 | const translation = {
159 | 'en' : {
160 | 'topHeroAnswer' : (names) => `The most used heroes lately are ${joinOr(names)}`,
161 | 'bestHeroThinking' : (names) => `Ok, let me check which heroes are good against ${joinOr(names,'or')}`,
162 | 'bestHeroAnswer' : (names) => {
163 | const isManyRec = names.length > 1
164 | return [
165 | `Humm, looks like ${isManyRec ? 'those heroes are good' : 'this hero is good'} for you to pick, ${joinOr(names,'or')}.`,
166 | `Try picking ${joinOr(names,'or')}, ${isManyRec ? 'they' : 'this hero'} seems to be a good pick.`,
167 | `Checking the latest matches, looks like picking ${joinOr(names,'or')} ${isManyRec ? 'are' : 'is'} a good option.`
168 | ]
169 | }
170 | },
171 | 'pt-br': {
172 | 'topHeroAnswer' : (names) => `Os herois mais usados ultimamente são ${joinOr(names)}`,
173 | 'bestHeroThinking' : (names) => `Ok, deixe me checar aqui quais herois são bons contra ${joinOr(names)}`,
174 | 'bestHeroAnswer' : (names) => {
175 | const isManyRec = names.length > 1
176 | return [
177 | `Humm, parece que ${isManyRec ? 'esses herois são bons' : 'esse heroi é bom'} para você pegar, ${joinOr(names)}.`,
178 | `Parece q se você pegar ${joinOr(names)} você vai ter boas chances.`,
179 | `Olhando as últimas partidas, parece que ${joinOr(names)} ${isManyRec ? 'são bons' : 'é bom'} neste cenário.`
180 | ]
181 | }
182 | }
183 | }
184 |
185 | exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
186 | const agent = new WebhookClient({ request, response })
187 | console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
188 | console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
189 |
190 | function welcome(agent) {
191 | agent.add(`Welcome to my agent!`);
192 | }
193 |
194 | function fallback(agent) {
195 | agent.add(`I didn't understand`);
196 | agent.add(`I'm sorry, can you try again?`);
197 | }
198 |
199 | /**
200 | * Best Hero Intent
201 | * @param {WebhookClient} agent
202 | */
203 | async function bestHeroHandler(agent){
204 | const { locale } = agent
205 | const { heroes } = agent.parameters
206 | //const isMany = heroes.length > 1
207 |
208 | async function loadHeroesList(ids){
209 | const heroesRef = db.ref('/heroes')
210 | const heroesPromises = ids.map( async (id) => {
211 | const heroSnap = await heroesRef.child(id).once('value')
212 | return heroSnap.val()
213 | })
214 | return Promise.all(heroesPromises)
215 | }
216 |
217 | const heroesData = await loadHeroesList(heroes)
218 | const names = heroesData.map( h => h.name )
219 | agent.add(translation[locale].bestHeroThinking(names))
220 |
221 | // 1º Strategy - Intersection
222 | const worstHeroesSet = heroesData.map( hero => {
223 | const { id, worstHeroes } = hero
224 | const worstHeroesIds = Object.keys(worstHeroes)
225 | const set = new Set([...worstHeroesIds])
226 | return { id, set }
227 | })
228 |
229 | const intersections = {}
230 | worstHeroesSet.forEach( heroA => {
231 | worstHeroesSet.forEach( heroB => {
232 | if(heroA.id === heroB.id){
233 | return
234 | }
235 | const setA = heroA.set
236 | const setB = heroB.set
237 | const intersection = new Set(
238 | [...setA].filter(x => setB.has(x)))
239 | const heroesList = Array.from(intersection.values())
240 | heroesList.forEach( heroId => {
241 | intersections[heroId] = intersections[heroId] || 0
242 | intersections[heroId] += 1
243 | })
244 | })
245 | })
246 |
247 | const hasIntersections = Object.keys(intersections).length > 0
248 | let heroesIds = []
249 | if(hasIntersections){
250 | heroesIds = Object.keys(intersections)
251 | }else{
252 | const allHeroes = heroesData.map( hero => Object.values(hero.worstHeroes))
253 | .reduce( (arr, list ) => arr.concat(list) , [])
254 | allHeroes.sort( (a,b) => b.advantage - a.advantage)
255 | const topHeroes = new Set()
256 | allHeroes.forEach( hero => {
257 | const len = topHeroes.size
258 | if( !topHeroes.has(hero.id) && len < 3){
259 | topHeroes.add(hero.id)
260 | }
261 | })
262 | heroesIds = Array.from(topHeroes.values())
263 | }
264 |
265 | const goodHeroes = await loadHeroesList(heroesIds)
266 | const goodHeroesNames = goodHeroes.map( h => h.name )
267 | agent.add(pickOne(translation[locale].bestHeroAnswer(goodHeroesNames)))
268 | }
269 |
270 | async function topHeroHandler(agent){
271 |
272 | const allHeroesSnap = await db.ref('/heroes').once('value')
273 | const allHeroes = allHeroesSnap.val()
274 |
275 | const allHeroesData = Object.values(allHeroes)
276 | allHeroesData.sort( (a,b) => a.rank - b.rank )
277 |
278 | const topHeroes = allHeroesData.slice(0,3)
279 | const topHeroesNames = topHeroes.map( h => h.name )
280 | agent.add(`Os herois mais usados ultimamente são ${joinOr(topHeroesNames)}`)
281 | }
282 |
283 | let intentMap = new Map();
284 | intentMap.set('Default Welcome Intent', welcome);
285 | intentMap.set('Default Fallback Intent', fallback);
286 | intentMap.set('BestHero', bestHeroHandler);
287 | intentMap.set('TopHero', topHeroHandler)
288 | agent.handleRequest(intentMap);
289 | });
290 |
--------------------------------------------------------------------------------
/utils/db.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "value": "abaddon",
4 | "synonyms": [
5 | "Abaddon"
6 | ]
7 | },
8 | {
9 | "value": "alchemist",
10 | "synonyms": [
11 | "Alchemist"
12 | ]
13 | },
14 | {
15 | "value": "ancient-apparition",
16 | "synonyms": [
17 | "Ancient Apparition",
18 | "Ancient",
19 | "Apparition"
20 | ]
21 | },
22 | {
23 | "value": "anti-mage",
24 | "synonyms": [
25 | "Anti-Mage"
26 | ]
27 | },
28 | {
29 | "value": "arc-warden",
30 | "synonyms": [
31 | "Arc Warden",
32 | "Arc",
33 | "Warden"
34 | ]
35 | },
36 | {
37 | "value": "axe",
38 | "synonyms": [
39 | "Axe"
40 | ]
41 | },
42 | {
43 | "value": "bane",
44 | "synonyms": [
45 | "Bane"
46 | ]
47 | },
48 | {
49 | "value": "batrider",
50 | "synonyms": [
51 | "Batrider"
52 | ]
53 | },
54 | {
55 | "value": "beastmaster",
56 | "synonyms": [
57 | "Beastmaster"
58 | ]
59 | },
60 | {
61 | "value": "bloodseeker",
62 | "synonyms": [
63 | "Bloodseeker"
64 | ]
65 | },
66 | {
67 | "value": "bounty-hunter",
68 | "synonyms": [
69 | "Bounty Hunter",
70 | "Bounty",
71 | "Hunter"
72 | ]
73 | },
74 | {
75 | "value": "brewmaster",
76 | "synonyms": [
77 | "Brewmaster"
78 | ]
79 | },
80 | {
81 | "value": "bristleback",
82 | "synonyms": [
83 | "Bristleback"
84 | ]
85 | },
86 | {
87 | "value": "broodmother",
88 | "synonyms": [
89 | "Broodmother"
90 | ]
91 | },
92 | {
93 | "value": "centaur-warrunner",
94 | "synonyms": [
95 | "Centaur Warrunner",
96 | "Centaur",
97 | "Warrunner"
98 | ]
99 | },
100 | {
101 | "value": "chaos-knight",
102 | "synonyms": [
103 | "Chaos Knight",
104 | "Chaos",
105 | "Knight"
106 | ]
107 | },
108 | {
109 | "value": "chen",
110 | "synonyms": [
111 | "Chen"
112 | ]
113 | },
114 | {
115 | "value": "clinkz",
116 | "synonyms": [
117 | "Clinkz"
118 | ]
119 | },
120 | {
121 | "value": "clockwerk",
122 | "synonyms": [
123 | "Clockwerk"
124 | ]
125 | },
126 | {
127 | "value": "crystal-maiden",
128 | "synonyms": [
129 | "Crystal Maiden",
130 | "Crystal",
131 | "Maiden"
132 | ]
133 | },
134 | {
135 | "value": "dark-seer",
136 | "synonyms": [
137 | "Dark Seer",
138 | "Dark",
139 | "Seer"
140 | ]
141 | },
142 | {
143 | "value": "dark-willow",
144 | "synonyms": [
145 | "Dark Willow",
146 | "Willow"
147 | ]
148 | },
149 | {
150 | "value": "dazzle",
151 | "synonyms": [
152 | "Dazzle"
153 | ]
154 | },
155 | {
156 | "value": "death-prophet",
157 | "synonyms": [
158 | "Death Prophet",
159 | "Death",
160 | "Prophet"
161 | ]
162 | },
163 | {
164 | "value": "disruptor",
165 | "synonyms": [
166 | "Disruptor"
167 | ]
168 | },
169 | {
170 | "value": "doom",
171 | "synonyms": [
172 | "Doom"
173 | ]
174 | },
175 | {
176 | "value": "dragon-knight",
177 | "synonyms": [
178 | "Dragon Knight",
179 | "Dragon"
180 | ]
181 | },
182 | {
183 | "value": "drow-ranger",
184 | "synonyms": [
185 | "Drow Ranger",
186 | "Drow",
187 | "Ranger"
188 | ]
189 | },
190 | {
191 | "value": "earth-spirit",
192 | "synonyms": [
193 | "Earth Spirit",
194 | "Earth",
195 | "Spirit"
196 | ]
197 | },
198 | {
199 | "value": "earthshaker",
200 | "synonyms": [
201 | "Earthshaker"
202 | ]
203 | },
204 | {
205 | "value": "elder-titan",
206 | "synonyms": [
207 | "Elder Titan",
208 | "Elder",
209 | "Titan"
210 | ]
211 | },
212 | {
213 | "value": "ember-spirit",
214 | "synonyms": [
215 | "Ember Spirit",
216 | "Ember"
217 | ]
218 | },
219 | {
220 | "value": "enchantress",
221 | "synonyms": [
222 | "Enchantress"
223 | ]
224 | },
225 | {
226 | "value": "enigma",
227 | "synonyms": [
228 | "Enigma"
229 | ]
230 | },
231 | {
232 | "value": "faceless-void",
233 | "synonyms": [
234 | "Faceless Void",
235 | "Faceless",
236 | "Void"
237 | ]
238 | },
239 | {
240 | "value": "grimstroke",
241 | "synonyms": [
242 | "Grimstroke"
243 | ]
244 | },
245 | {
246 | "value": "gyrocopter",
247 | "synonyms": [
248 | "Gyrocopter"
249 | ]
250 | },
251 | {
252 | "value": "huskar",
253 | "synonyms": [
254 | "Huskar"
255 | ]
256 | },
257 | {
258 | "value": "invoker",
259 | "synonyms": [
260 | "Invoker"
261 | ]
262 | },
263 | {
264 | "value": "io",
265 | "synonyms": [
266 | "Io"
267 | ]
268 | },
269 | {
270 | "value": "jakiro",
271 | "synonyms": [
272 | "Jakiro"
273 | ]
274 | },
275 | {
276 | "value": "juggernaut",
277 | "synonyms": [
278 | "Juggernaut"
279 | ]
280 | },
281 | {
282 | "value": "keeper-of-the-light",
283 | "synonyms": [
284 | "Keeper of the Light",
285 | "Keeper",
286 | "Light"
287 | ]
288 | },
289 | {
290 | "value": "kunkka",
291 | "synonyms": [
292 | "Kunkka"
293 | ]
294 | },
295 | {
296 | "value": "legion-commander",
297 | "synonyms": [
298 | "Legion Commander",
299 | "Legion",
300 | "Commander"
301 | ]
302 | },
303 | {
304 | "value": "leshrac",
305 | "synonyms": [
306 | "Leshrac"
307 | ]
308 | },
309 | {
310 | "value": "lich",
311 | "synonyms": [
312 | "Lich"
313 | ]
314 | },
315 | {
316 | "value": "lifestealer",
317 | "synonyms": [
318 | "Lifestealer"
319 | ]
320 | },
321 | {
322 | "value": "lina",
323 | "synonyms": [
324 | "Lina"
325 | ]
326 | },
327 | {
328 | "value": "lion",
329 | "synonyms": [
330 | "Lion"
331 | ]
332 | },
333 | {
334 | "value": "lone-druid",
335 | "synonyms": [
336 | "Lone Druid",
337 | "Lone",
338 | "Druid"
339 | ]
340 | },
341 | {
342 | "value": "luna",
343 | "synonyms": [
344 | "Luna"
345 | ]
346 | },
347 | {
348 | "value": "lycan",
349 | "synonyms": [
350 | "Lycan"
351 | ]
352 | },
353 | {
354 | "value": "magnus",
355 | "synonyms": [
356 | "Magnus"
357 | ]
358 | },
359 | {
360 | "value": "mars",
361 | "synonyms": [
362 | "Mars"
363 | ]
364 | },
365 | {
366 | "value": "medusa",
367 | "synonyms": [
368 | "Medusa"
369 | ]
370 | },
371 | {
372 | "value": "meepo",
373 | "synonyms": [
374 | "Meepo"
375 | ]
376 | },
377 | {
378 | "value": "mirana",
379 | "synonyms": [
380 | "Mirana"
381 | ]
382 | },
383 | {
384 | "value": "monkey-king",
385 | "synonyms": [
386 | "Monkey King",
387 | "Monkey",
388 | "King"
389 | ]
390 | },
391 | {
392 | "value": "morphling",
393 | "synonyms": [
394 | "Morphling"
395 | ]
396 | },
397 | {
398 | "value": "naga-siren",
399 | "synonyms": [
400 | "Naga Siren",
401 | "Naga",
402 | "Siren"
403 | ]
404 | },
405 | {
406 | "value": "natures-prophet",
407 | "synonyms": [
408 | "Nature's Prophet",
409 | "Nature's"
410 | ]
411 | },
412 | {
413 | "value": "necrophos",
414 | "synonyms": [
415 | "Necrophos"
416 | ]
417 | },
418 | {
419 | "value": "night-stalker",
420 | "synonyms": [
421 | "Night Stalker",
422 | "Night",
423 | "Stalker"
424 | ]
425 | },
426 | {
427 | "value": "nyx-assassin",
428 | "synonyms": [
429 | "Nyx Assassin",
430 | "Nyx",
431 | "Assassin"
432 | ]
433 | },
434 | {
435 | "value": "ogre-magi",
436 | "synonyms": [
437 | "Ogre Magi",
438 | "Ogre",
439 | "Magi"
440 | ]
441 | },
442 | {
443 | "value": "omniknight",
444 | "synonyms": [
445 | "Omniknight"
446 | ]
447 | },
448 | {
449 | "value": "oracle",
450 | "synonyms": [
451 | "Oracle"
452 | ]
453 | },
454 | {
455 | "value": "outworld-devourer",
456 | "synonyms": [
457 | "Outworld Devourer",
458 | "Outworld",
459 | "Devourer"
460 | ]
461 | },
462 | {
463 | "value": "pangolier",
464 | "synonyms": [
465 | "Pangolier"
466 | ]
467 | },
468 | {
469 | "value": "phantom-assassin",
470 | "synonyms": [
471 | "Phantom Assassin",
472 | "Phantom"
473 | ]
474 | },
475 | {
476 | "value": "phantom-lancer",
477 | "synonyms": [
478 | "Phantom Lancer",
479 | "Lancer"
480 | ]
481 | },
482 | {
483 | "value": "phoenix",
484 | "synonyms": [
485 | "Phoenix"
486 | ]
487 | },
488 | {
489 | "value": "puck",
490 | "synonyms": [
491 | "Puck"
492 | ]
493 | },
494 | {
495 | "value": "pudge",
496 | "synonyms": [
497 | "Pudge"
498 | ]
499 | },
500 | {
501 | "value": "pugna",
502 | "synonyms": [
503 | "Pugna"
504 | ]
505 | },
506 | {
507 | "value": "queen-of-pain",
508 | "synonyms": [
509 | "Queen of Pain",
510 | "Queen",
511 | "Pain"
512 | ]
513 | },
514 | {
515 | "value": "razor",
516 | "synonyms": [
517 | "Razor"
518 | ]
519 | },
520 | {
521 | "value": "riki",
522 | "synonyms": [
523 | "Riki"
524 | ]
525 | },
526 | {
527 | "value": "rubick",
528 | "synonyms": [
529 | "Rubick"
530 | ]
531 | },
532 | {
533 | "value": "sand-king",
534 | "synonyms": [
535 | "Sand King",
536 | "Sand"
537 | ]
538 | },
539 | {
540 | "value": "shadow-demon",
541 | "synonyms": [
542 | "Shadow Demon",
543 | "Shadow",
544 | "Demon"
545 | ]
546 | },
547 | {
548 | "value": "shadow-fiend",
549 | "synonyms": [
550 | "Shadow Fiend",
551 | "Fiend"
552 | ]
553 | },
554 | {
555 | "value": "shadow-shaman",
556 | "synonyms": [
557 | "Shadow Shaman",
558 | "Shaman"
559 | ]
560 | },
561 | {
562 | "value": "silencer",
563 | "synonyms": [
564 | "Silencer"
565 | ]
566 | },
567 | {
568 | "value": "skywrath-mage",
569 | "synonyms": [
570 | "Skywrath Mage",
571 | "Skywrath",
572 | "Mage"
573 | ]
574 | },
575 | {
576 | "value": "slardar",
577 | "synonyms": [
578 | "Slardar"
579 | ]
580 | },
581 | {
582 | "value": "slark",
583 | "synonyms": [
584 | "Slark"
585 | ]
586 | },
587 | {
588 | "value": "snapfire",
589 | "synonyms": [
590 | "Snapfire"
591 | ]
592 | },
593 | {
594 | "value": "sniper",
595 | "synonyms": [
596 | "Sniper"
597 | ]
598 | },
599 | {
600 | "value": "spectre",
601 | "synonyms": [
602 | "Spectre"
603 | ]
604 | },
605 | {
606 | "value": "spirit-breaker",
607 | "synonyms": [
608 | "Spirit Breaker",
609 | "Breaker"
610 | ]
611 | },
612 | {
613 | "value": "storm-spirit",
614 | "synonyms": [
615 | "Storm Spirit",
616 | "Storm"
617 | ]
618 | },
619 | {
620 | "value": "sven",
621 | "synonyms": [
622 | "Sven"
623 | ]
624 | },
625 | {
626 | "value": "techies",
627 | "synonyms": [
628 | "Techies"
629 | ]
630 | },
631 | {
632 | "value": "templar-assassin",
633 | "synonyms": [
634 | "Templar Assassin",
635 | "Templar"
636 | ]
637 | },
638 | {
639 | "value": "terrorblade",
640 | "synonyms": [
641 | "Terrorblade"
642 | ]
643 | },
644 | {
645 | "value": "tidehunter",
646 | "synonyms": [
647 | "Tidehunter"
648 | ]
649 | },
650 | {
651 | "value": "timbersaw",
652 | "synonyms": [
653 | "Timbersaw"
654 | ]
655 | },
656 | {
657 | "value": "tinker",
658 | "synonyms": [
659 | "Tinker"
660 | ]
661 | },
662 | {
663 | "value": "tiny",
664 | "synonyms": [
665 | "Tiny"
666 | ]
667 | },
668 | {
669 | "value": "treant-protector",
670 | "synonyms": [
671 | "Treant Protector",
672 | "Treant",
673 | "Protector"
674 | ]
675 | },
676 | {
677 | "value": "troll-warlord",
678 | "synonyms": [
679 | "Troll Warlord",
680 | "Troll",
681 | "Warlord"
682 | ]
683 | },
684 | {
685 | "value": "tusk",
686 | "synonyms": [
687 | "Tusk"
688 | ]
689 | },
690 | {
691 | "value": "underlord",
692 | "synonyms": [
693 | "Underlord"
694 | ]
695 | },
696 | {
697 | "value": "undying",
698 | "synonyms": [
699 | "Undying"
700 | ]
701 | },
702 | {
703 | "value": "ursa",
704 | "synonyms": [
705 | "Ursa"
706 | ]
707 | },
708 | {
709 | "value": "vengeful-spirit",
710 | "synonyms": [
711 | "Vengeful Spirit",
712 | "Vengeful"
713 | ]
714 | },
715 | {
716 | "value": "venomancer",
717 | "synonyms": [
718 | "Venomancer"
719 | ]
720 | },
721 | {
722 | "value": "viper",
723 | "synonyms": [
724 | "Viper"
725 | ]
726 | },
727 | {
728 | "value": "visage",
729 | "synonyms": [
730 | "Visage"
731 | ]
732 | },
733 | {
734 | "value": "void-spirit",
735 | "synonyms": [
736 | "Void Spirit"
737 | ]
738 | },
739 | {
740 | "value": "warlock",
741 | "synonyms": [
742 | "Warlock"
743 | ]
744 | },
745 | {
746 | "value": "weaver",
747 | "synonyms": [
748 | "Weaver"
749 | ]
750 | },
751 | {
752 | "value": "windranger",
753 | "synonyms": [
754 | "Windranger"
755 | ]
756 | },
757 | {
758 | "value": "winter-wyvern",
759 | "synonyms": [
760 | "Winter Wyvern",
761 | "Winter",
762 | "Wyvern"
763 | ]
764 | },
765 | {
766 | "value": "witch-doctor",
767 | "synonyms": [
768 | "Witch Doctor",
769 | "Witch",
770 | "Doctor"
771 | ]
772 | },
773 | {
774 | "value": "wraith-king",
775 | "synonyms": [
776 | "Wraith King",
777 | "Wraith"
778 | ]
779 | },
780 | {
781 | "value": "zeus",
782 | "synonyms": [
783 | "Zeus"
784 | ]
785 | }
786 | ]
--------------------------------------------------------------------------------