├── 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 |
24 | 25 |
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 | 28 | Select Hero 29 | setSearch(evt.target.value)} /> 36 |
37 | 38 | {filteredHeroes.map( hero => { 39 | return ( 40 | onSelected(hero.id)}/> 45 | ) 46 | })} 47 | 48 |
49 |
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 | {hero.name} 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 | 2 | 3 | 4 | 5 | 6 | 7 | 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 | {hero.name} 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 | ] --------------------------------------------------------------------------------