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 e17fd6d..04176fb 100644 Binary files a/ios/memelee.xcodeproj/project.xcworkspace/xcuserdata/laika.xcuserdatad/UserInterfaceState.xcuserstate and b/ios/memelee.xcodeproj/project.xcworkspace/xcuserdata/laika.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios/memelee/Info.plist b/ios/memelee/Info.plist index 41b99e1..593408c 100644 --- a/ios/memelee/Info.plist +++ b/ios/memelee/Info.plist @@ -26,14 +26,10 @@ 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); + }); +};