From c02ad19f2ba5158e9def56a2dde7f6337440c360 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 11 Apr 2018 23:16:10 -0400 Subject: [PATCH] More fixes/patches --- controllers/AttendeesListViewController.js | 34 ++ controllers/BracketViewController.js | 26 +- controllers/ContactViewController.js | 34 ++ controllers/EventInfoViewController.js | 115 +++--- controllers/GenericInfoViewController.js | 28 ++ controllers/LocationViewController.js | 34 ++ controllers/TournamentInfoViewController.js | 108 +++++- controllers/TournamentsListViewController.js | 3 +- controllers/components/TournamentRow.js | 3 +- index.js | 22 +- .../UserInterfaceState.xcuserstate | Bin 48408 -> 23796 bytes ios/memelee/Info.plist | 12 +- package.json | 2 + stores/TournamentEventStore.js | 326 ++++++++++-------- stores/index.js | 2 +- styles/index.js | 13 + utils/Constants.js | 24 ++ utils/index.js | 35 ++ 18 files changed, 582 insertions(+), 239 deletions(-) create mode 100644 controllers/AttendeesListViewController.js create mode 100644 controllers/ContactViewController.js create mode 100644 controllers/GenericInfoViewController.js create mode 100644 controllers/LocationViewController.js create mode 100644 utils/Constants.js create mode 100644 utils/index.js diff --git a/controllers/AttendeesListViewController.js b/controllers/AttendeesListViewController.js new file mode 100644 index 0000000..ec1e4e6 --- /dev/null +++ b/controllers/AttendeesListViewController.js @@ -0,0 +1,34 @@ +/** + * AttendeesListViewController + * + * Yeah, I do it iOS style. Handles fetching and displaying upcoming tournaments. + * + * @copyright Ryan McGrath 2018. + */ + +import moment from 'moment'; +import React from 'react'; +import {FlatList, View, ActivityIndicator} from 'react-native'; +import {inject, observer} from 'mobx-react/native'; +import {SearchBar} from 'react-native-elements'; + +import styles from '../styles'; +import Constants from '../utils/Constants'; +import MemeleeViewController from './MemeleeViewController'; +import TournamentRow from './components/TournamentRow'; + +const keyExtractor = (item, index) => item.id; + +export default class AttendeesListViewController extends MemeleeViewController { + renderItem = ({item}) => () + + render() { + const props = { + data: this.props.data, + keyExtractor: keyExtractor, + renderItem: this.renderItem + }; + + return + } +} diff --git a/controllers/BracketViewController.js b/controllers/BracketViewController.js index 968ec47..9247047 100644 --- a/controllers/BracketViewController.js +++ b/controllers/BracketViewController.js @@ -9,9 +9,8 @@ import moment from 'moment'; import React from 'react'; import {ScrollView, Text, View} from 'react-native'; -import {v4} from 'uuid'; +import {inject, observer} from 'mobx-react/native'; -//import SmashGG from '../store'; import MemeleeViewController from './MemeleeViewController'; const Match = ({set, ...rest}) => ( @@ -22,32 +21,17 @@ const Match = ({set, ...rest}) => ( ); +@inject('Events') @observer export default class BracketViewController extends MemeleeViewController { - state = { - brackets: { - winners: [], - losers: [], - grandFinals: [] - } - }; - componentWillMount() { - const evtSlugs = this.props.evt.slug.split('/'); - const evtSlug = evtSlugs.length > 0 ? evtSlugs[evtSlugs.length - 1] : null; - const tournamentSlug = this.props.tournament.slugs[0].replace('tournament/', ''); - - SmashGG.fetchBracketData(tournamentSlug, evtSlug, this.props.bracket.id).then(this.updateBracketsData).catch(console.error); + this.props.Events.fetchBracketData(this.props.bracket.id); } - - updateBracketsData = (brackets) => { - this.setState({brackets: brackets}); - } - + render() { return ( {['winners', 'losers'].map(key => ( - {this.state.brackets[key].map(bracket => ( + {this.props.Events.bracketData[key].map(bracket => ( {bracket.title} {bracket.sets.map(set => )} diff --git a/controllers/ContactViewController.js b/controllers/ContactViewController.js new file mode 100644 index 0000000..ec1e4e6 --- /dev/null +++ b/controllers/ContactViewController.js @@ -0,0 +1,34 @@ +/** + * AttendeesListViewController + * + * Yeah, I do it iOS style. Handles fetching and displaying upcoming tournaments. + * + * @copyright Ryan McGrath 2018. + */ + +import moment from 'moment'; +import React from 'react'; +import {FlatList, View, ActivityIndicator} from 'react-native'; +import {inject, observer} from 'mobx-react/native'; +import {SearchBar} from 'react-native-elements'; + +import styles from '../styles'; +import Constants from '../utils/Constants'; +import MemeleeViewController from './MemeleeViewController'; +import TournamentRow from './components/TournamentRow'; + +const keyExtractor = (item, index) => item.id; + +export default class AttendeesListViewController extends MemeleeViewController { + renderItem = ({item}) => () + + render() { + const props = { + data: this.props.data, + keyExtractor: keyExtractor, + renderItem: this.renderItem + }; + + return + } +} diff --git a/controllers/EventInfoViewController.js b/controllers/EventInfoViewController.js index 35ae812..506572b 100644 --- a/controllers/EventInfoViewController.js +++ b/controllers/EventInfoViewController.js @@ -8,40 +8,61 @@ import moment from 'moment'; import React from 'react'; -import {ScrollView, Image, Text, View, TouchableOpacity} from 'react-native'; +import {ScrollView, StyleSheet, ActivityIndicator, Image, Text, View, TouchableOpacity} from 'react-native'; +import {inject, observer} from 'mobx-react/native'; import Markdown from 'react-native-simple-markdown' -//import SmashGG from '../store'; +import SegmentedControlTab from 'react-native-segmented-control-tab'; +import SettingsList, {Header, Item} from 'react-native-settings-list'; +import styles from '../styles'; +import Constants from '../utils/Constants'; import MemeleeViewController from './MemeleeViewController'; +const Loading = (props) => ( + + + +); + +const Standings = (props) => ( + props.error ? + No Standings Found + Matches may not have been played yet. + : + + Players + Losses + + + {props.standings.map(standing => ( + + + {standing.standing} + + {standing.name} + {standing.losses.map(loss => {loss})} + + ))} + + +); + +const s = StyleSheet.flatten(styles.tournamentDetailsEventWrapper); +const Brackets = (props) => ( + + {props.brackets.map(bracket => ( + props.onPress(bracket)} /> + ))} + +); + +@inject('Events') @observer export default class EventInfoViewController extends MemeleeViewController { - state = { - standings: [], - brackets: [] - }; - - componentWillMount() { - const evtSlugs = this.props.evt.slug.split('/'); - const evtSlug = evtSlugs.length > 0 ? evtSlugs[evtSlugs.length - 1] : null; - const tournamentSlug = this.props.tournament.slugs[0].replace('tournament/', ''); - - if((evtSlug && evtSlug !== '') && (tournamentSlug && tournamentSlug != '')) { - SmashGG.fetchEventExpanded(tournamentSlug, evtSlug).then(this.updateBracketsData).catch(console.error); - SmashGG.fetchEventStandings(tournamentSlug, evtSlug).then(this.updateStandingsData).catch(console.error); - } - } - - updateBracketsData = (data) => { - this.setState({brackets: data}); - } - - updateStandingsData = (data) => { - this.setState({standings: data}); - } + state = {selectedIndex: 0}; onBracketPress = (bracket) => { this.props.navigator.push({ - screen: 'memelee.bracket', + screen: Constants.Screens.Bracket, title: bracket.name, backButtonTitle: 'Back', passProps: { @@ -53,29 +74,25 @@ export default class EventInfoViewController extends MemeleeViewController { }); } - render() { - return ( - Brackets - {this.state.brackets.map(bracket => ( - this.onBracketPress(bracket)}> - {bracket.name} - - ))} + swapIndex = (index) => { + this.setState({selectedIndex: index}); + } - Standings - - Players - Losses - - {this.state.standings.map(standing => ( - - - {standing.finalPlacement} - - {standing.name} - {standing.losses.map(loss => {loss})} - - ))} - ); + render() { + return ( + + + {this.state.selectedIndex === 0 ? + this.props.Events.fetchingStandingData ? + : + : null + } + + {this.state.selectedIndex === 1 ? + this.props.Events.fetchingBracketData ? + : + : null + } + ); } } diff --git a/controllers/GenericInfoViewController.js b/controllers/GenericInfoViewController.js new file mode 100644 index 0000000..17f7b0d --- /dev/null +++ b/controllers/GenericInfoViewController.js @@ -0,0 +1,28 @@ +/** + * AttendeesListViewController + * + * Yeah, I do it iOS style. Handles fetching and displaying upcoming tournaments. + * + * @copyright Ryan McGrath 2018. + */ + +import React from 'react'; +import {ScrollView, View} from 'react-native'; +import Markdown from 'react-native-markdown-renderer'; +import Hyperlink from 'react-native-hyperlink'; + +import styles from '../styles'; +import Constants from '../utils/Constants'; +import MemeleeViewController from './MemeleeViewController'; + +export default class GenericInfoViewController extends MemeleeViewController { + render = () => ( + + + + {this.props.info && this.props.info !== '' ? this.props.info : ''} + + + + ); +} diff --git a/controllers/LocationViewController.js b/controllers/LocationViewController.js new file mode 100644 index 0000000..ec1e4e6 --- /dev/null +++ b/controllers/LocationViewController.js @@ -0,0 +1,34 @@ +/** + * AttendeesListViewController + * + * Yeah, I do it iOS style. Handles fetching and displaying upcoming tournaments. + * + * @copyright Ryan McGrath 2018. + */ + +import moment from 'moment'; +import React from 'react'; +import {FlatList, View, ActivityIndicator} from 'react-native'; +import {inject, observer} from 'mobx-react/native'; +import {SearchBar} from 'react-native-elements'; + +import styles from '../styles'; +import Constants from '../utils/Constants'; +import MemeleeViewController from './MemeleeViewController'; +import TournamentRow from './components/TournamentRow'; + +const keyExtractor = (item, index) => item.id; + +export default class AttendeesListViewController extends MemeleeViewController { + renderItem = ({item}) => () + + render() { + const props = { + data: this.props.data, + keyExtractor: keyExtractor, + renderItem: this.renderItem + }; + + return + } +} diff --git a/controllers/TournamentInfoViewController.js b/controllers/TournamentInfoViewController.js index 0a9f8b7..85538bd 100644 --- a/controllers/TournamentInfoViewController.js +++ b/controllers/TournamentInfoViewController.js @@ -9,12 +9,15 @@ import moment from 'moment'; import React from 'react'; import {ScrollView, StyleSheet, Image, Text, View, TouchableOpacity, Dimensions} from 'react-native'; -//import Markdown from 'react-native-simple-markdown' -import Markdown from 'react-native-markdown-renderer'; +import Markdown, {PluginContainer} from 'react-native-markdown-renderer'; import SegmentedControlTab from 'react-native-segmented-control-tab'; import SettingsList, {Header, Item} from 'react-native-settings-list'; +import linkify from 'linkify-it'; import styles from '../styles'; +import Constants from '../utils/Constants'; +import {openURL, parseSlugs} from '../utils'; +import EventsStore from '../stores/TournamentEventStore'; import MemeleeViewController from './MemeleeViewController'; const w = Dimensions.get('screen').width; @@ -31,41 +34,126 @@ const w = Dimensions.get('screen').width; export default class TournamentInfoViewController extends MemeleeViewController { state = { - selectedIndex: 0 + selectedIndex: 0, + tabs: [] }; + componentWillMount = () => { + const tabs = [ + {slug: 'attendees', name: 'Attendees', screen: Constants.Screens.Attendees, adminOnly: false}, + {slug: 'location', name: 'Location', screen: Constants.Screens.Location, adminOnly: false}, + {slug: 'contact', name: 'Contact', screen: Constants.Screens.Contact, adminOnly: false} + ]; + + if(this.props.tournament.rules && this.props.tournament.rules !== '') { + if(this.props.tournament.rules.startsWith('http://') || this.props.tournament.rules.startsWith('https://')) { + tabs.push({slug: 'rules', name: 'Rules', url: this.props.tournament.rules, adminOnly: false}); + } else { + tabs.push({slug: 'rules', name: 'Rules', info: this.props.tournament.rules, adminOnly: false}); + } + } + + if(this.props.tournament.prizes && this.props.tournament.prizes !== '') { + if(this.props.tournament.prizes.startsWith('http://') || this.props.tournament.prizes.startsWith('https://')) { + tabs.push({slug: 'prizes', name: 'Prizes', url: this.props.tournament.prizes, adminOnly: false}); + } else { + tabs.push({slug: 'prizes', name: 'Prizes', info: this.props.tournament.prizes, adminOnly: false}); + } + } + + if(this.props.tournament.publishing && this.props.tournament.publishing.fantasy) + tabs.push({ + slug: 'fantasy', + name: 'Fantasy', + url: 'https://smash.gg/tournament/' + parseSlugs(this.props.tournament, null).tournament + '/fantasy/', + adminOnly: false + }); + + const generatedTabs = this.props.tournament.generatedTabs; + if(generatedTabs) { + const objs = Object.keys(generatedTabs).map(key => generatedTabs[key]); + + objs.forEach(tab => { + Object.keys(tab).map(key => ({ + slug: key, + name: tab[key].name, + adminOnly: tab[key].adminOnly + })).filter(tab => !tab.adminOnly || tab.adminOnly === false).forEach(tab => tabs.push(tab)); + }); + } + + this.setState({tabs: tabs}); + } + onEventTapped = (evt) => { + EventsStore.loadEventData(this.props.tournament, evt); this.props.navigator.push({ - screen: 'memelee.tournamentEventInfoScreen', + screen: Constants.Screens.TournamentEventInfoScreen, title: evt.name, passProps: {tournament: this.props.tournament, evt: evt}, navigatorStyle: {tabBarHidden: true} }); } + handleTab = (tab) => { + if(tab.screen) + return this.props.navigator.push({ + screen: tab.screen, + title: tab.name, + passProps: {data: []}, + navigatorStyle: {tabBarHidden: true} + }); + + if(tab.url) + return openURL(tab.url); + + if(tab.info) + return this.props.navigator.push({ + screen: Constants.Screens.Info, + title: tab.name, + passProps: {info: tab.info}, + navigatorStyle: {tabBarHidden: true} + }); + } + swapIndex = (index) => { this.setState({ selectedIndex: index }); } + + plugins = [] render() { const s = StyleSheet.flatten(styles.tournamentDetailsEventWrapper); + const ss = { + itemWidth: 50, + backgroundColor: s.backgroundColor, + style: styles.tournamentDetailsEventWrapper, + titleStyle: styles.tournamentDetailsEventItem + }; return ( - {this.state.selectedIndex === 0 ? ( - - {this.props.tournament.details && this.props.tournament.details !== '' ? this.props.tournament.details : ''} - - ) : null} + {this.state.selectedIndex === 0 ? ( + + + {this.props.tournament.details && this.props.tournament.details !== '' ? this.props.tournament.details : ''} + + + + + {this.state.tabs.map(tab => this.handleTab(tab)} />)} + + + ) : null} {this.state.selectedIndex === 1 ? ( {this.props.tournament.memeleeEvents.map(evt => ( - + this.onEventTapped(evt)} /> ))} ) : null} ); diff --git a/controllers/TournamentsListViewController.js b/controllers/TournamentsListViewController.js index afd64e7..5c70af4 100644 --- a/controllers/TournamentsListViewController.js +++ b/controllers/TournamentsListViewController.js @@ -13,6 +13,7 @@ import {inject, observer} from 'mobx-react/native'; import {SearchBar} from 'react-native-elements'; import styles from '../styles'; +import Constants from '../utils/Constants'; import MemeleeViewController from './MemeleeViewController'; import TournamentRow from './components/TournamentRow'; @@ -26,7 +27,7 @@ export default class UpcomingTournamentsViewController extends MemeleeViewContro onTap = (tournament) => { this.props.navigator.push({ - screen: 'memelee.tournamentInfoScreen', + screen: Constants.Screens.TournamentInfoScreen, title: tournament.name, passProps: {tournament: tournament}, navigatorStyle: {tabBarHidden: true} diff --git a/controllers/components/TournamentRow.js b/controllers/components/TournamentRow.js index a23541f..a6c6f70 100644 --- a/controllers/components/TournamentRow.js +++ b/controllers/components/TournamentRow.js @@ -37,8 +37,7 @@ export default class TournamentRow extends React.Component { {this.props.tournament.hasOnlineEvents && (!this.props.tournament.city || this.props.tournament.city === '') ? 'Online' : (this.props.tournament.city ? this.props.tournament.city + ', ' : '') + this.props.tournament.addrState} - - {this.props.tournament.memeleeEventsCount} Events + {this.props.tournament.memeleeEventsCount} Events {this.props.tournament.attendeeCount} Attendees diff --git a/index.js b/index.js index 0810099..d72af51 100644 --- a/index.js +++ b/index.js @@ -13,30 +13,28 @@ import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import styles from './styles'; import Stores from './stores'; +import Constants from './utils/Constants'; import Provider from './utils/MobxRnnProvider'; import TournamentsListViewController from './controllers/TournamentsListViewController'; import TournamentInfoViewController from './controllers/TournamentInfoViewController'; +import AttendeesListViewController from './controllers/AttendeesListViewController'; +import LocationViewController from './controllers/LocationViewController'; +import ContactViewController from './controllers/ContactViewController'; +import GenericInfoViewController from './controllers/GenericInfoViewController'; import EventInfoViewController from './controllers/EventInfoViewController'; import BracketViewController from './controllers//BracketViewController'; import BookmarksViewController from './controllers/BookmarksViewController'; import SettingsViewController from './controllers/settings'; -const Constants = { - Screens: { - TournamentsList: 'memelee.tournamentsList', - TournamentInfoScreen: 'memelee.tournamentInfoScreen', - TournamentEventInfoScreen: 'memelee.tournamentEventInfoScreen', - Bracket: 'memelee.tournamentBracket', - Bookmarks: 'memelee.bookmarks', - Settings: 'memelee.settings' - } -}; - Navigation.registerComponent(Constants.Screens.TournamentsList, () => TournamentsListViewController, Stores, Provider); Navigation.registerComponent(Constants.Screens.TournamentInfoScreen, () => TournamentInfoViewController, Stores, Provider); Navigation.registerComponent(Constants.Screens.TournamentEventInfoScreen, () => EventInfoViewController, Stores, Provider); -Navigation.registerComponent(Constants.Screens.TournamentEventBracket, () => BracketViewController, Stores, Provider); +Navigation.registerComponent(Constants.Screens.Attendees, () => AttendeesListViewController, Stores, Provider); +Navigation.registerComponent(Constants.Screens.Location, () => LocationViewController, Stores, Provider); +Navigation.registerComponent(Constants.Screens.Contact, () => ContactViewController, Stores, Provider); +Navigation.registerComponent(Constants.Screens.Info, () => GenericInfoViewController, Stores, Provider); +Navigation.registerComponent(Constants.Screens.Bracket, () => BracketViewController, Stores, Provider); Navigation.registerComponent(Constants.Screens.Bookmarks, () => BookmarksViewController, Stores, Provider); Navigation.registerComponent(Constants.Screens.Settings, () => SettingsViewController, Stores, Provider); diff --git a/ios/memelee.xcodeproj/project.xcworkspace/xcuserdata/laika.xcuserdatad/UserInterfaceState.xcuserstate b/ios/memelee.xcodeproj/project.xcworkspace/xcuserdata/laika.xcuserdatad/UserInterfaceState.xcuserstate index e17fd6da3f31c21c06a489d9a06a5751bf4e71a6..04176fb9aac6ad7dcb79392a697164e0fa604773 100644 GIT binary patch delta 10119 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 S79syPSM#i^efE_x_x}K<8wUme literal 48408 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 a7# NSAppTransportSecurity - NSExceptionDomains - - localhost - - NSExceptionAllowsInsecureHTTPLoads - - - + NSThirdPartyExceptionRequiresForwardSecrecy + + NSAllowsArbitraryLoads + NSLocationWhenInUseUsageDescription diff --git a/package.json b/package.json index 75daf48..1714b5b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "axios": "^0.18.0", "babel-preset-react-native-stage-0": "^1.0.1", "jsog": "^1.0.7", + "linkify-it": "^2.0.3", "mobx": "^4.1.1", "mobx-persist": "^0.4.1", "mobx-react": "^5.0.0", @@ -29,6 +30,7 @@ "react-controllables": "^0.6.0", "react-native": "0.52.0", "react-native-elements": "^0.19.0", + "react-native-hyperlink": "0.0.12", "react-native-markdown-renderer": "^3.1.0", "react-native-navigation": "^1.1.426", "react-native-segmented-control-tab": "^3.2.2", diff --git a/stores/TournamentEventStore.js b/stores/TournamentEventStore.js index 5d97dde..e44618b 100644 --- a/stores/TournamentEventStore.js +++ b/stores/TournamentEventStore.js @@ -6,166 +6,222 @@ * @copyright Ryan McGrath 2018 */ +import {v4} from 'uuid'; import {observable, action, runInAction} from 'mobx'; +import {parseSlugs} from '../utils'; class Store { - @observable data; + @observable.ref phases; + @observable.ref bracketData; @observable fetchingData; + + @observable.ref standings; + @observable fetchingStandingData; + @observable standingsError; constructor() { - this.data = {}; + this.phases = []; + this.standings = []; + this.bracketData = {winners: [], losers: []}; + this.tournamentSlug = null; + this.evtSlug = null; + this.fetchingData = false; + this.fetchingStandingData = false; + this.standingsError = false; } - fetchEventExpanded = async (tournamentSlug, eventSlug, opts) => { - const api = 'https://smash.gg/api/-/gg_api./tournament/' + tournamentSlug + '/event/' + eventSlug + ';'; - const args = Object.assign({ + parseSlugs = (tournament, evt) => { + const slugs = parseSlugs(tournament, evt); + this.tournamentSlug = slugs.tournament; + this.evtSlug = slugs.evt; + } + + loadEventData = async(tournament, evt) => { + this.phases = []; + this.standings = []; + this.bracketData = {winners: [], losers: []}; + this.tournamentSlug = null; + this.evtSlug = null; + + this.parseSlugs(tournament, evt); + + if(this.tournamentSlug && this.evtSlug) { + this.fetchPhases(); + this.fetchStandings(); + } + } + + /** + * fetchPhases + * + * Given a set of slugs, fetches event phases. + */ + fetchPhases = () => { + const api = 'https://smash.gg/api/-/gg_api./tournament/' + this.tournamentSlug + '/event/' + this.evtSlug + ';'; + const args = { expand: JSON.stringify(['phase', 'groups']), reset: false, - slug: tournamentSlug, - eventSlug: eventSlug - }, opts || {}); + slug: this.tournamentSlug, + eventSlug: this.evtSlug + }; - return fetch( - api + Object.keys(args).map(key => `${key}=${args[key]}`).join(';') + '?returnMeta=true' - ).then(response => response.json()).then(data => { + fetch(api + Object.keys(args).map(key => `${key}=${args[key]}`).join(';') + '?returnMeta=true').then( + response => response.json() + ).then(data => { if(typeof data.success !== 'undefined' && !data.success) throw new Error(data.message); - return Promise.resolve(data.entities.phase ? data.entities.phase.map(phase => phase) : []); + runInAction('Parse out Phases...', () => { + if(data.entities.phase) + this.phases = data.entities.phase.map(phase => phase); + }); }); } -/** - * fetchStandings - * - * Given a set of slugs, fetches event standings. - * - * @arg tournamentSlug {String} Slug for the tournament (e.g, "the-mango"). - * @arg eventSlug {String} Slug for the event (e.g, "melee-singles"). - * @arg opts {Object} Optional object for overriding request properties. - * @return Promise - */ -/*const fetchEventStandings = (tournamentSlug, eventSlug, opts) => { - const api = 'https://smash.gg/api/-/gg_api./tournament/' + tournamentSlug + '/event/' + eventSlug + '/standings;'; - const args = Object.assign({ - entityId: null, - entityType: 'event', - slug: tournamentSlug, - eventSlug: eventSlug, - expand: JSON.stringify(['entrants', 'standingGroup', 'attendee']), - mutations: JSON.stringify(['playerData', 'standingLosses']), - page: 1, - per_page: 25 - }, opts || {}); - - return fetch( - api + Object.keys(args).map(key => `${key}=${args[key]}`).join(';') + '?returnMeta=true' - ).then(response => response.json()).then(data => { - var s = data.items.entities.standing, - l = s.length; - - return Promise.resolve(data.items.entities.entrants.map(entrants => { - if(typeof entrants.losses === 'undefined') - entrants.losses = []; - - data.items.entities.standing.forEach(standing => { - if(standing.entityId === entrants.id && standing.mutations) - entrants.losses = entrants.losses.concat(standing.mutations.losses.map(loss => loss.name)); - }); - - return entrants; - }).sort((a, b) => a.finalPlacement > b.finalPlacement)); - }); -}; - -/** - * fetchBracketData - * - * Given a set of slugs/bracket ID, fetches bracket data for rendering. Performs a lot - * of smaller operations to transpose it into an actually usable format for display. - * - * @arg tournamentSlug {String} Slug for the tournament (e.g, "the-mango"). - * @arg eventSlug {String} Slug for the event (e.g, "melee-singles"). - * @arg bracketID {String or Number} ID for the bracket - SmashGG calls this a phase. Go fig. - * @arg opts {Object} Optional object for overriding request properties. - * @return Promise - -const fetchBracketData = (tournamentSlug, eventSlug, bracketID, opts) => { - const api = 'https://smash.gg/api/-/gg_api./tournament/' + tournamentSlug + '/event/' + eventSlug + '/phase_groups;'; - const args = { - slug: tournamentSlug, - eventSlug: eventSlug, - expand: JSON.stringify(['results', 'character']), - mutations: JSON.stringify(['ffaData', 'playerData']), - filter: JSON.stringify({phaseId: bracketID}), - getSingleBracket: true, - page: 1, - per_page: 20, - reset: false, - }; - - return fetch( - api + Object.keys(args).map(key => `${key}=${args[key]}`).join(';') + '?returnMeta=true' - ).then(response => response.json()).then(data => { - const grands = []; - const brackets = { - winners: [], - losers: [] + /** + * fetchStandings + * + * Given a set of slugs, fetches event standings. + */ + fetchStandings = () => { + const api = 'https://smash.gg/api/-/gg_api./tournament/' + this.tournamentSlug + '/event/' + this.evtSlug + '/standings;'; + const args = { + entityId: null, + entityType: 'event', + slug: this.tournamentSlug, + eventSlug: this.evtSlug, + expand: JSON.stringify(['entrants', 'standingGroup', 'attendee']), + mutations: JSON.stringify(['playerData', 'standingLosses']), + page: 1, + per_page: 25 }; - // Filter through the set list and make sure each object is filled out with necessary data, - // and then place them into their bracket accordingly. Brackets will be sorted afterwards. - data.items.entities.sets.forEach(function(result) { - if(result.entrant1Id === null || result.entrant2Id === null) - return; + this.standings = []; + this.fetchingStandingsData = true; + fetch(api + Object.keys(args).map(key => `${key}=${args[key]}`).join(';') + '?returnMeta=true').then( + response => response.json() + ).then(data => { + runInAction('Parse out Standings...', () => { + var s = data.items.entities.standing, + l = s.length; - result.entrant1 = {}; - result.entrant2 = {}; + this.standingsError = false; + this.standings = data.items.entities.entrants.map(entrants => { + if(typeof entrants.losses === 'undefined') + entrants.losses = []; - data.items.entities.entrants.forEach(function(entrant) { - if(entrant.id === result.entrant1Id) - result.entrant1 = entrant; - - if(entrant.id === result.entrant2Id) - result.entrant2 = entrant; - }); + data.items.entities.standing.forEach(standing => { + if(standing.entityId === entrants.id) { + if(standing.mutations) + entrants.losses = entrants.losses.concat(standing.mutations.losses.map(loss => loss.name)); - if(result.isGF) { - grands.push(result); - } else { - var isLosers = result.displayRound < 0, - key = isLosers ? 'losers' : 'winners', - idx = isLosers ? (result.displayRound * -1) : result.displayRound; - - while(brackets[key].length < idx) { - brackets[key].push({ - title: '', // Filled in later~ - sets: [] + entrants.standing = standing.standing; + } }); - } - - brackets[key][idx - 1].title = result.fullRoundText; - brackets[key][idx - 1].sets.push(result); - if(!brackets[key][idx - 1].key) - brackets[key][idx - 1].key = v4(); - } - }); - // GFs are technically in the winners bracket, but for presentation purposes they're shoved - // in after to be like how smash.gg presents them. - if(grands.length > 0) { - grands.forEach(grandFinal => { - brackets.winners.push({ - title: 'Grand Finals', - sets: [grandFinal], - key: v4() - }); + return entrants; + }).sort((a, b) => a.finalPlacement > b.finalPlacement); + this.fetchingStandingsData = false; }); - } + }).catch(error => { + runInAction('Event standings data failed', () => { + this.standingsError = true; + this.fetchingStandingsData = false; + }); + }); + } - return Promise.resolve(brackets); - }); -};*/ -} + /** + * fetchBracketData + * + * Given a set of slugs/bracket ID, fetches bracket data for rendering. Performs a lot + * of smaller operations to transpose it into an actually usable format for display. + * + * @arg bracketID {String or Number} ID for the bracket - SmashGG calls this a phase. Go fig. + */ + fetchBracketData = (bracketID) => { + this.bracketData = {winners: [], losers: []}; + this.bracketID = bracketID; + + const api = 'https://smash.gg/api/-/gg_api./tournament/' + this.tournamentSlug + '/event/' + this.evtSlug + '/phase_groups;'; + const args = { + slug: this.tournamentSlug, + eventSlug: this.evtSlug, + expand: JSON.stringify(['results', 'character']), + mutations: JSON.stringify(['ffaData', 'playerData']), + filter: JSON.stringify({phaseId: bracketID}), + getSingleBracket: true, + page: 1, + per_page: 20, + reset: false, + }; + + const url = api + Object.keys(args).map(key => `${key}=${args[key]}`).join(';') + '?returnMeta=true' + console.log(url); + fetch( + url + ).then(response => response.json()).then(data => { + const grands = []; + const brackets = { + winners: [], + losers: [] + }; + + // Filter through the set list and make sure each object is filled out with necessary data, + // and then place them into their bracket accordingly. Brackets will be sorted afterwards. + data.items.entities.sets.forEach(function(result) { + if(result.entrant1Id === null || result.entrant2Id === null) + return; + + result.entrant1 = {}; + result.entrant2 = {}; + + data.items.entities.entrants.forEach(function(entrant) { + if(entrant.id === result.entrant1Id) + result.entrant1 = entrant; + + if(entrant.id === result.entrant2Id) + result.entrant2 = entrant; + }); + + if(result.isGF) { + grands.push(result); + } else { + var isLosers = result.displayRound < 0, + key = isLosers ? 'losers' : 'winners', + idx = isLosers ? (result.displayRound * -1) : result.displayRound; + + while(brackets[key].length < idx) { + brackets[key].push({ + title: '', // Filled in later~ + sets: [] + }); + } + + brackets[key][idx - 1].title = result.fullRoundText; + brackets[key][idx - 1].sets.push(result); + if(!brackets[key][idx - 1].key) + brackets[key][idx - 1].key = v4(); + } + }); + + // GFs are technically in the winners bracket, but for presentation purposes they're shoved + // in after to be like how smash.gg presents them. + if(grands.length > 0) { + grands.forEach(grandFinal => { + brackets.winners.push({ + title: 'Grand Finals', + sets: [grandFinal], + key: v4() + }); + }); + } + + runInAction('Set Bracket Data', () => { + this.bracketData = brackets; + }); + }); + } +}; export default new Store(); diff --git a/stores/index.js b/stores/index.js index 9b6520f..70c3660 100644 --- a/stores/index.js +++ b/stores/index.js @@ -23,7 +23,7 @@ const hydrate = create({ const stores = { Tournaments: TournamentsListingStore, - TournamentEventStore: TournamentEventStore, + Events: TournamentEventStore, Bookmarks: BookmarksStore, Settings: SettingsStore }; diff --git a/styles/index.js b/styles/index.js index a3322a7..461503c 100644 --- a/styles/index.js +++ b/styles/index.js @@ -108,6 +108,19 @@ const stylesheet = StyleSheet.create({ flex: 1, backgroundColor: iconColor, padding: 10, + }, + + eventsErrorTextHeader: { + textAlign: 'center', + color: textColor, + fontWeight: 'bold', + fontSize: 16 + }, + + eventsErrorText: { + textAlign: 'center', + marginTop: 5, + color: textColor } }); diff --git a/utils/Constants.js b/utils/Constants.js new file mode 100644 index 0000000..c1e1098 --- /dev/null +++ b/utils/Constants.js @@ -0,0 +1,24 @@ +/** + * Constants + * + * They're constants. CONSTANTS. CONSTANCE TURN THE MUSIC DOWN. + * + * @copyright Ryan McGrath 2018 + */ + +export default { + Screens: { + TournamentsList: 'memelee.tournamentsList', + TournamentInfoScreen: 'memelee.tournamentInfoScreen', + TournamentEventInfoScreen: 'memelee.tournamentEventInfoScreen', + + Attendees: 'memelee.attendeesListViewController', + Location: 'memelee.locationViewController', + Contact: 'memelee.contactViewController', + Info: 'memelee.infoViewController', + + Bracket: 'memelee.tournamentBracket', + Bookmarks: 'memelee.bookmarks', + Settings: 'memelee.settings' + } +}; diff --git a/utils/index.js b/utils/index.js new file mode 100644 index 0000000..81f01ef --- /dev/null +++ b/utils/index.js @@ -0,0 +1,35 @@ +/** + * Utils + * + * Various utilities used throughout the app. A kitchen junk drawer, if you will. + * + * @copyright Ryan McGrath 2018 + */ + +import {Linking} from 'react-native'; + +export const parseSlugs = (tournament, evt) => { + const evtSlugs = evt && evt.slug.split('/'); + const evtSlug = evt && evtSlugs.length > 0 ? evtSlugs[evtSlugs.length - 1] : null; + const tournamentSlug = ( + tournament.slug ? tournament.slug : + tournament.slugs && tournament.slugs.length ? tournament.slugs[0] : '' + ).replace('tournament/', ''); + + const slugs = {tournamentSlug: null, evtSlug: null}; + + if(tournamentSlug && tournamentSlug !== '') + slugs.tournament = tournamentSlug; + + if(evtSlug && evtSlug !== '') + slugs.evt = evtSlug; + + return slugs; +}; + +export const openURL = (url) => { + return Linking.canOpenURL(url).then(supported => { + if(supported) Linking.openURL(url); + else console.warn('Cannot open URL: ' + url); + }); +};