diff --git a/.gitignore b/.gitignore index 5a5060e..336547f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ ## Secrets ScoreSecrets/* +## API +GameAPI/** + ## User settings xcuserdata/ diff --git a/gameAPI/Package.swift b/gameAPI/Package.swift deleted file mode 100644 index 141f37d..0000000 --- a/gameAPI/Package.swift +++ /dev/null @@ -1,28 +0,0 @@ -// swift-tools-version:5.9 - -import PackageDescription - -let package = Package( - name: "GameAPI", - platforms: [ - .iOS(.v12), - .macOS(.v10_14), - .tvOS(.v12), - .watchOS(.v5), - ], - products: [ - .library(name: "GameAPI", targets: ["GameAPI"]), - ], - dependencies: [ - .package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.0.0"), - ], - targets: [ - .target( - name: "GameAPI", - dependencies: [ - .product(name: "ApolloAPI", package: "apollo-ios"), - ], - path: "./Sources" - ), - ] -) diff --git a/gameAPI/Sources/.DS_Store b/gameAPI/Sources/.DS_Store deleted file mode 100644 index 8de1a40..0000000 Binary files a/gameAPI/Sources/.DS_Store and /dev/null differ diff --git a/gameAPI/Sources/Operations/.DS_Store b/gameAPI/Sources/Operations/.DS_Store deleted file mode 100644 index 4ffd9d2..0000000 Binary files a/gameAPI/Sources/Operations/.DS_Store and /dev/null differ diff --git a/gameAPI/Sources/Operations/Queries/GamesQuery.graphql.swift b/gameAPI/Sources/Operations/Queries/GamesQuery.graphql.swift deleted file mode 100644 index 33c8929..0000000 --- a/gameAPI/Sources/Operations/Queries/GamesQuery.graphql.swift +++ /dev/null @@ -1,120 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -@_exported import ApolloAPI - -public class GamesQuery: GraphQLQuery { - public static let operationName: String = "Games" - public static let operationDocument: ApolloAPI.OperationDocument = .init( - definition: .init( - #"query Games { games { __typename id city date gender location opponentId result sport state time scoreBreakdown team { __typename id color image name } boxScore { __typename team period time description scorer assist scoreBy corScore oppScore } } }"# - )) - - public init() {} - - public struct Data: GameAPI.SelectionSet { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public static var __parentType: any ApolloAPI.ParentType { GameAPI.Objects.Query } - public static var __selections: [ApolloAPI.Selection] { [ - .field("games", [Game?]?.self), - ] } - - public var games: [Game?]? { __data["games"] } - - /// Game - /// - /// Parent Type: `GameType` - public struct Game: GameAPI.SelectionSet { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public static var __parentType: any ApolloAPI.ParentType { GameAPI.Objects.GameType } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("id", String?.self), - .field("city", String.self), - .field("date", String.self), - .field("gender", String.self), - .field("location", String?.self), - .field("opponentId", String.self), - .field("result", String?.self), - .field("sport", String.self), - .field("state", String.self), - .field("time", String?.self), - .field("scoreBreakdown", [[String?]?]?.self), - .field("team", Team?.self), - .field("boxScore", [BoxScore?]?.self), - ] } - - public var id: String? { __data["id"] } - public var city: String { __data["city"] } - public var date: String { __data["date"] } - public var gender: String { __data["gender"] } - public var location: String? { __data["location"] } - public var opponentId: String { __data["opponentId"] } - public var result: String? { __data["result"] } - public var sport: String { __data["sport"] } - public var state: String { __data["state"] } - public var time: String? { __data["time"] } - public var scoreBreakdown: [[String?]?]? { __data["scoreBreakdown"] } - public var team: Team? { __data["team"] } - public var boxScore: [BoxScore?]? { __data["boxScore"] } - - /// Game.Team - /// - /// Parent Type: `TeamType` - public struct Team: GameAPI.SelectionSet { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public static var __parentType: any ApolloAPI.ParentType { GameAPI.Objects.TeamType } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("id", String?.self), - .field("color", String.self), - .field("image", String?.self), - .field("name", String.self), - ] } - - public var id: String? { __data["id"] } - public var color: String { __data["color"] } - public var image: String? { __data["image"] } - public var name: String { __data["name"] } - } - - /// Game.BoxScore - /// - /// Parent Type: `BoxScore` - public struct BoxScore: GameAPI.SelectionSet { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public static var __parentType: any ApolloAPI.ParentType { GameAPI.Objects.BoxScore } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("team", String?.self), - .field("period", String?.self), - .field("time", String?.self), - .field("description", String?.self), - .field("scorer", String?.self), - .field("assist", String?.self), - .field("scoreBy", String?.self), - .field("corScore", Int?.self), - .field("oppScore", Int?.self), - ] } - - public var team: String? { __data["team"] } - public var period: String? { __data["period"] } - public var time: String? { __data["time"] } - public var description: String? { __data["description"] } - public var scorer: String? { __data["scorer"] } - public var assist: String? { __data["assist"] } - public var scoreBy: String? { __data["scoreBy"] } - public var corScore: Int? { __data["corScore"] } - public var oppScore: Int? { __data["oppScore"] } - } - } - } -} diff --git a/gameAPI/Sources/Operations/Queries/GetTeamByIdQuery.graphql.swift b/gameAPI/Sources/Operations/Queries/GetTeamByIdQuery.graphql.swift deleted file mode 100644 index b8b7fc2..0000000 --- a/gameAPI/Sources/Operations/Queries/GetTeamByIdQuery.graphql.swift +++ /dev/null @@ -1,54 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -@_exported import ApolloAPI - -public class GetTeamByIdQuery: GraphQLQuery { - public static let operationName: String = "GetTeamById" - public static let operationDocument: ApolloAPI.OperationDocument = .init( - definition: .init( - #"query GetTeamById($id: String!) { team(id: $id) { __typename id color image name } }"# - )) - - public var id: String - - public init(id: String) { - self.id = id - } - - public var __variables: Variables? { ["id": id] } - - public struct Data: GameAPI.SelectionSet { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public static var __parentType: any ApolloAPI.ParentType { GameAPI.Objects.Query } - public static var __selections: [ApolloAPI.Selection] { [ - .field("team", Team?.self, arguments: ["id": .variable("id")]), - ] } - - public var team: Team? { __data["team"] } - - /// Team - /// - /// Parent Type: `TeamType` - public struct Team: GameAPI.SelectionSet { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public static var __parentType: any ApolloAPI.ParentType { GameAPI.Objects.TeamType } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("id", String?.self), - .field("color", String.self), - .field("image", String?.self), - .field("name", String.self), - ] } - - public var id: String? { __data["id"] } - public var color: String { __data["color"] } - public var image: String? { __data["image"] } - public var name: String { __data["name"] } - } - } -} diff --git a/gameAPI/Sources/Schema/.DS_Store b/gameAPI/Sources/Schema/.DS_Store deleted file mode 100644 index 034f828..0000000 Binary files a/gameAPI/Sources/Schema/.DS_Store and /dev/null differ diff --git a/gameAPI/Sources/Schema/Objects/BoxScore.graphql.swift b/gameAPI/Sources/Schema/Objects/BoxScore.graphql.swift deleted file mode 100644 index b63d512..0000000 --- a/gameAPI/Sources/Schema/Objects/BoxScore.graphql.swift +++ /dev/null @@ -1,23 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Objects { - /// Represents an individual entry in the box score of a game. - /// - /// Attributes: - /// - `team`: The team involved in the scoring event. - /// - `period`: The period or inning of the event. - /// - `time`: The time of the scoring event. - /// - `description`: A description of the play or scoring event. - /// - `scorer`: The name of the scorer. - /// - `assist`: The name of the assisting player. - /// - `score_by`: Indicates which team scored. - /// - `cor_score`: Cornell's score at the time of the event. - /// - `opp_score`: Opponent's score at the time of the event. - static let BoxScore = ApolloAPI.Object( - typename: "BoxScore", - implementedInterfaces: [] - ) -} \ No newline at end of file diff --git a/gameAPI/Sources/Schema/Objects/BoxScoreEntryType.graphql.swift b/gameAPI/Sources/Schema/Objects/BoxScoreEntryType.graphql.swift deleted file mode 100644 index 2e05521..0000000 --- a/gameAPI/Sources/Schema/Objects/BoxScoreEntryType.graphql.swift +++ /dev/null @@ -1,23 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Objects { - /// Represents an individual entry in the box score of a game. - /// - /// Attributes: - /// - `team`: The team involved in the scoring event. - /// - `period`: The period or inning of the event. - /// - `time`: The time of the scoring event. - /// - `description`: A description of the play or scoring event. - /// - `scorer`: The name of the scorer. - /// - `assist`: The name of the assisting player. - /// - `score_by`: Indicates which team scored. - /// - `cor_score`: Cornell's score at the time of the event. - /// - `opp_score`: Opponent's score at the time of the event. - static let BoxScoreEntryType = ApolloAPI.Object( - typename: "BoxScoreEntryType", - implementedInterfaces: [] - ) -} \ No newline at end of file diff --git a/gameAPI/Sources/Schema/Objects/GameType.graphql.swift b/gameAPI/Sources/Schema/Objects/GameType.graphql.swift deleted file mode 100644 index 77c1659..0000000 --- a/gameAPI/Sources/Schema/Objects/GameType.graphql.swift +++ /dev/null @@ -1,26 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Objects { - /// A GraphQL type representing a game. - /// - /// Attributes: - /// - `id`: The ID of the game (optional). - /// - `city`: The city of the game. - /// - `date`: The date of the game. - /// - `gender`: The gender of the game. - /// - `location`: The location of the game. (optional) - /// - `opponent_id`: The id of the opposing team. - /// - `result`: The result of the game. (optional) - /// - `sport`: The sport of the game. - /// - `state`: The state of the game. - /// - `time`: The time of the game. (optional) - /// - `box_score`: The box score of the game. - /// - `score_breakdown`: The score breakdown of the game. - static let GameType = ApolloAPI.Object( - typename: "GameType", - implementedInterfaces: [] - ) -} \ No newline at end of file diff --git a/gameAPI/Sources/Schema/Objects/Query.graphql.swift b/gameAPI/Sources/Schema/Objects/Query.graphql.swift deleted file mode 100644 index a4155e4..0000000 --- a/gameAPI/Sources/Schema/Objects/Query.graphql.swift +++ /dev/null @@ -1,11 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Objects { - static let Query = ApolloAPI.Object( - typename: "Query", - implementedInterfaces: [] - ) -} \ No newline at end of file diff --git a/gameAPI/Sources/Schema/Objects/TeamType.graphql.swift b/gameAPI/Sources/Schema/Objects/TeamType.graphql.swift deleted file mode 100644 index e714216..0000000 --- a/gameAPI/Sources/Schema/Objects/TeamType.graphql.swift +++ /dev/null @@ -1,18 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Objects { - /// A GraphQL type representing a team. - /// - /// Attributes: - /// - `id`: The ID of the team (optional). - /// - `color`: The color of the team. - /// - `image`: The image of the team (optional). - /// - `name`: The name of the team. - static let TeamType = ApolloAPI.Object( - typename: "TeamType", - implementedInterfaces: [] - ) -} \ No newline at end of file diff --git a/gameAPI/Sources/Schema/SchemaConfiguration.swift b/gameAPI/Sources/Schema/SchemaConfiguration.swift deleted file mode 100644 index 8723501..0000000 --- a/gameAPI/Sources/Schema/SchemaConfiguration.swift +++ /dev/null @@ -1,15 +0,0 @@ -// @generated -// This file was automatically generated and can be edited to -// provide custom configuration for a generated GraphQL schema. -// -// Any changes to this file will not be overwritten by future -// code generation execution. - -import ApolloAPI - -public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration { - public static func cacheKeyInfo(for type: ApolloAPI.Object, object: ApolloAPI.ObjectData) -> CacheKeyInfo? { - // Implement this function to configure cache key resolution for your schema types. - return nil - } -} diff --git a/gameAPI/Sources/Schema/SchemaMetadata.graphql.swift b/gameAPI/Sources/Schema/SchemaMetadata.graphql.swift deleted file mode 100644 index 4805c9e..0000000 --- a/gameAPI/Sources/Schema/SchemaMetadata.graphql.swift +++ /dev/null @@ -1,34 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public protocol SelectionSet: ApolloAPI.SelectionSet & ApolloAPI.RootSelectionSet -where Schema == GameAPI.SchemaMetadata {} - -public protocol InlineFragment: ApolloAPI.SelectionSet & ApolloAPI.InlineFragment -where Schema == GameAPI.SchemaMetadata {} - -public protocol MutableSelectionSet: ApolloAPI.MutableRootSelectionSet -where Schema == GameAPI.SchemaMetadata {} - -public protocol MutableInlineFragment: ApolloAPI.MutableSelectionSet & ApolloAPI.InlineFragment -where Schema == GameAPI.SchemaMetadata {} - -public enum SchemaMetadata: ApolloAPI.SchemaMetadata { - public static let configuration: any ApolloAPI.SchemaConfiguration.Type = SchemaConfiguration.self - - public static func objectType(forTypename typename: String) -> ApolloAPI.Object? { - switch typename { - case "BoxScore": return GameAPI.Objects.BoxScore - case "GameType": return GameAPI.Objects.GameType - case "Query": return GameAPI.Objects.Query - case "TeamType": return GameAPI.Objects.TeamType - default: return nil - } - } -} - -public enum Objects {} -public enum Interfaces {} -public enum Unions {} diff --git a/score-ios.xcodeproj/project.pbxproj b/score-ios.xcodeproj/project.pbxproj index 9003159..4d15f42 100644 --- a/score-ios.xcodeproj/project.pbxproj +++ b/score-ios.xcodeproj/project.pbxproj @@ -79,6 +79,7 @@ D8B1C9D12CD2CE3D0095E563 /* PastGamesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B1C9D02CD2CE3C0095E563 /* PastGamesView.swift */; }; D8B1C9D32CD2D20A0095E563 /* PastGameTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B1C9D22CD2D20A0095E563 /* PastGameTile.swift */; }; D8DD4E642CFD48ED00F2C46E /* Team.graphql in Resources */ = {isa = PBXBuildFile; fileRef = D8DD4E632CFD48E400F2C46E /* Team.graphql */; }; + FD27F4232DC0A68900CC172E /* GamesCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD27F4222DC0A68900CC172E /* GamesCacheManager.swift */; }; FD5A38DB2D8F2BDD00CF5E30 /* GameLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5A38DA2D8F2BDD00CF5E30 /* GameLoadingView.swift */; }; FD5A38DD2D8F30CC00CF5E30 /* GameErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5A38DC2D8F30CC00CF5E30 /* GameErrorView.swift */; }; FD5A38DF2D8F3E1400CF5E30 /* ShimmerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5A38DE2D8F3E1400CF5E30 /* ShimmerModifier.swift */; }; @@ -178,6 +179,7 @@ D8B1C9D02CD2CE3C0095E563 /* PastGamesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PastGamesView.swift; sourceTree = ""; }; D8B1C9D22CD2D20A0095E563 /* PastGameTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PastGameTile.swift; sourceTree = ""; }; D8DD4E632CFD48E400F2C46E /* Team.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = Team.graphql; sourceTree = ""; }; + FD27F4222DC0A68900CC172E /* GamesCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GamesCacheManager.swift; sourceTree = ""; }; FD5A38DA2D8F2BDD00CF5E30 /* GameLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameLoadingView.swift; sourceTree = ""; }; FD5A38DC2D8F30CC00CF5E30 /* GameErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameErrorView.swift; sourceTree = ""; }; FD5A38DE2D8F3E1400CF5E30 /* ShimmerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShimmerModifier.swift; sourceTree = ""; }; @@ -475,6 +477,7 @@ children = ( D89102122CF10CA5004CE226 /* NetworkManager.swift */, D891020F2CF0EBA0004CE226 /* DataCoordinator.swift */, + FD27F4222DC0A68900CC172E /* GamesCacheManager.swift */, ); path = Networking; sourceTree = ""; @@ -570,7 +573,7 @@ mainGroup = CE725D2F2C89120100386943; packageReferences = ( D89102012CED68D9004CE226 /* XCRemoteSwiftPackageReference "apollo-ios" */, - D89102162CF151DD004CE226 /* XCLocalSwiftPackageReference "gameAPI" */, + FD602E2D2DC11D4A002711BE /* XCLocalSwiftPackageReference "GameAPI" */, ); productRefGroup = CE725D392C89120200386943 /* Products */; projectDirPath = ""; @@ -689,6 +692,7 @@ CE8ED4FA2D6BF46300A274DE /* Sex.swift in Sources */, D8B1C9D12CD2CE3D0095E563 /* PastGamesView.swift in Sources */, CE335CD32C922E8D0037F572 /* PrimaryColors.swift in Sources */, + FD27F4232DC0A68900CC172E /* GamesCacheManager.swift in Sources */, CE725D3C2C89120200386943 /* Home.swift in Sources */, FD5A38DF2D8F3E1400CF5E30 /* ShimmerModifier.swift in Sources */, CE528FA02C96420700C238B5 /* PickerView.swift in Sources */, @@ -1040,9 +1044,9 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - D89102162CF151DD004CE226 /* XCLocalSwiftPackageReference "gameAPI" */ = { + FD602E2D2DC11D4A002711BE /* XCLocalSwiftPackageReference "GameAPI" */ = { isa = XCLocalSwiftPackageReference; - relativePath = gameAPI; + relativePath = GameAPI; }; /* End XCLocalSwiftPackageReference section */ diff --git a/score-ios/Models/DummyData.swift b/score-ios/Models/DummyData.swift index 1a575ba..9df92f7 100644 --- a/score-ios/Models/DummyData.swift +++ b/score-ios/Models/DummyData.swift @@ -16,7 +16,8 @@ extension Game { // Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Princeton", state: "NJ", date: Date.dateComponents(year: 2024, month: 5, day: 20, hour: 10, minute: 0), sport: .Basketball, address: "2 Fake St", sex: .Women, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "New Haven", state: "CT", date: Date.dateComponents(year: 2024, month: 5, day: 22, hour: 10, minute: 0), sport: .Soccer, address: "3 Fake St", sex: .Women, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), // Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Providence", state: "RI", date: Date.dateComponents(year: 2024, month: 5, day: 23, hour: 10, minute: 0), sport: .CrossCountry, address: "4 Fake St", sex: .Women, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), - Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Hanover", state: "NH", date: Date.dateComponents(year: 2024, month: 5, day: 24, hour: 10, minute: 0), sport: .IceHockey, address: "5 Fake St", sex: .Women, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), + Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Hanover", state: "NH", date: Date.dateComponents(year: 2024, month: 5, day: 24, hour: 10, minute: 0), sport: .Baseball + , address: "5 Fake St", sex: .Women, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "New York", state: "NY", date: Date.dateComponents(year: 2024, month: 5, day: 25, hour: 10, minute: 0), sport: .Lacrosse, address: "6 Fake St", sex: .Women, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), // Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Pennsylvania", state: "PA", date: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 10, minute: 0), sport: .Basketball, address: "0 Fake St", sex: .Men, timeUpdates: [], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), @@ -24,7 +25,7 @@ extension Game { // Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Princeton", state: "NJ", date: Date.dateComponents(year: 2024, month: 5, day: 20, hour: 10, minute: 0), sport: .Basketball, address: "2 Fake St", sex: .Men, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "New Haven", state: "CT", date: Date.dateComponents(year: 2024, month: 5, day: 22, hour: 10, minute: 0), sport: .Soccer, address: "3 Fake St", sex: .Men, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), // Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Providence", state: "RI", date: Date.dateComponents(year: 2024, month: 5, day: 23, hour: 10, minute: 0), sport: .CrossCountry, address: "4 Fake St", sex: .Men, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), - Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Hanover", state: "NH", date: Date.dateComponents(year: 2024, month: 5, day: 24, hour: 10, minute: 0), sport: .IceHockey, address: "5 Fake St", sex: .Men, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), + Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "Hanover", state: "NH", date: Date.dateComponents(year: 2024, month: 5, day: 24, hour: 10, minute: 0), sport: .Baseball, address: "5 Fake St", sex: .Men, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), Game(opponent: Team(id: "673d2c20569abe4465e9f792", color: "blue", image: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Cornell_University_seal.svg/1200px-Cornell_University_seal.svg.png", name: "Cornell"), city: "New York", state: "NY", date: Date.dateComponents(year: 2024, month: 5, day: 25, hour: 10, minute: 0), sport: .Lacrosse, address: "6 Fake St", sex: .Men, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2024", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]) ] // static let dummyData: [Game] = [ diff --git a/score-ios/Models/Game.swift b/score-ios/Models/Game.swift index 9c5565e..9ed6f80 100644 --- a/score-ios/Models/Game.swift +++ b/score-ios/Models/Game.swift @@ -18,7 +18,7 @@ protocol GameType : Identifiable where ID == UUID { var sport: Sport { get } var sex: Sex { get } - // TODO add more, maybe longitude and latitude for Transit integration? Idk + // TODO add more, maybe longitude and latitude for Transit integration var address: String { get } var timeUpdates: [TimeUpdate] { get } @@ -27,6 +27,7 @@ protocol GameType : Identifiable where ID == UUID { struct Game : GameType, Identifiable { var id: UUID = UUID() + var serverId: String? var opponent: Team var city: String var state: String @@ -43,6 +44,7 @@ struct Game : GameType, Identifiable { } init(game: GamesQuery.Data.Game) { + self.serverId = game.id self.city = game.city self.state = game.state self.date = Date.parseDate(dateString: game.date, timeString: game.time ?? "12:00 p.m.") @@ -102,10 +104,11 @@ extension Game { oppTotal += oppScore updates.append(timeUpdate) } - if (index == corScores.count - 1) { - let total = TimeUpdate(timestamp: index + 1, isTotal: true, cornellScore: corTotal, opponentScore: oppTotal) - updates.append(total) - } + +// if (index == corScores.count - 1) { +// let total = TimeUpdate(timestamp: index + 1, isTotal: true, cornellScore: corTotal, opponentScore: oppTotal) +// updates.append(total) +// } }) } } diff --git a/score-ios/Models/GraphQL/Game.graphql b/score-ios/Models/GraphQL/Game.graphql index 251c9b5..03decfe 100644 --- a/score-ios/Models/GraphQL/Game.graphql +++ b/score-ios/Models/GraphQL/Game.graphql @@ -1,32 +1,45 @@ -query Games { - games { - id - city - date - gender - location - opponentId - result - sport - state - time - scoreBreakdown - team { - id - color - image - name - } - boxScore { - team - period - time - description - scorer - assist - scoreBy - corScore - oppScore - } - } +fragment TeamFragment on TeamType { + id + color + image + name +} + +fragment BoxScoreEntryFragment on BoxScoreEntryType { + team + period + time + description + scorer + assist + scoreBy + corScore + oppScore +} + +fragment GameFragment on GameType { + id + city + date + gender + location + opponentId + result + sport + state + time + scoreBreakdown + utcDate + team { + ...TeamFragment + } + boxScore { + ...BoxScoreEntryFragment + } +} + +query Games($limit: Int!, $offset: Int!) { + games(limit: $limit, offset: $offset) { + ...GameFragment + } } diff --git a/score-ios/Models/GraphQL/schema.graphqls b/score-ios/Models/GraphQL/schema.graphqls index bd9e8e4..76c410d 100644 --- a/score-ios/Models/GraphQL/schema.graphqls +++ b/score-ios/Models/GraphQL/schema.graphqls @@ -25,7 +25,13 @@ directive @defer( type Query { youtubeVideos: [YoutubeVideoType] youtubeVideo(id: String!): YoutubeVideoType - games: [GameType] + games( + """Number of games to return""" + limit: Int = 100 + + """Number of games to skip""" + offset: Int = 0 + ): [GameType] game(id: String!): GameType gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!): GameType gamesBySport(sport: String!): [GameType] @@ -52,6 +58,7 @@ type YoutubeVideoType { title: String! description: String! thumbnail: String! + b64Thumbnail: String! url: String! publishedAt: String! } @@ -84,9 +91,10 @@ type GameType { sport: String! state: String! time: String - boxScore: [BoxScore] + boxScore: [BoxScoreEntryType] scoreBreakdown: [[String]] team: TeamType + utcDate: String } """ @@ -103,7 +111,7 @@ Attributes: - `cor_score`: Cornell's score at the time of the event. - `opp_score`: Opponent's score at the time of the event. """ -type BoxScore { +type BoxScoreEntryType { team: String period: String time: String @@ -122,24 +130,26 @@ Attributes: - `id`: The ID of the team (optional). - `color`: The color of the team. - `image`: The image of the team (optional). + - `b64_image`: The base64 encoded image of the team (optional). - `name`: The name of the team. """ type TeamType { id: String color: String! image: String + b64Image: String name: String! } type Mutation { """Creates a new game.""" - createGame(boxScore: String, city: String!, date: String!, gender: String!, location: String, opponentId: String!, result: String, scoreBreakdown: String, sport: String!, state: String!, time: String!): CreateGame + createGame(boxScore: String, city: String!, date: String!, gender: String!, location: String, opponentId: String!, result: String, scoreBreakdown: String, sport: String!, state: String!, time: String!, utcDate: String): CreateGame """Creates a new team.""" - createTeam(color: String!, image: String, name: String!): CreateTeam + createTeam(b64Image: String, color: String!, image: String, name: String!): CreateTeam """Creates a new youtube video.""" - createYoutubeVideo(description: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo + createYoutubeVideo(b64Thumbnail: String!, description: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo } type CreateGame { @@ -152,4 +162,4 @@ type CreateTeam { type CreateYoutubeVideo { youtubeVideo: YoutubeVideoType -} +} \ No newline at end of file diff --git a/score-ios/Models/Sport.swift b/score-ios/Models/Sport.swift index ebdf88f..837a2fa 100644 --- a/score-ios/Models/Sport.swift +++ b/score-ios/Models/Sport.swift @@ -16,7 +16,7 @@ enum Sport : String, Identifiable, CaseIterable, CustomStringConvertible { // Both // case Basketball // case CrossCountry - case IceHockey +// case IceHockey case Lacrosse case Soccer // case Squash @@ -64,8 +64,8 @@ enum Sport : String, Identifiable, CaseIterable, CustomStringConvertible { // return "Basketball" // case .CrossCountry: // return "Cross Country" - case .IceHockey: - return "Ice Hockey" +// case .IceHockey: +// return "Ice Hockey" case .Lacrosse: return "Lacrosse" case .Soccer: diff --git a/score-ios/Networking/GamesCacheManager.swift b/score-ios/Networking/GamesCacheManager.swift new file mode 100644 index 0000000..c96b1e4 --- /dev/null +++ b/score-ios/Networking/GamesCacheManager.swift @@ -0,0 +1,233 @@ +// +// GamesCacheManager.swift +// score-ios +// +// Created by Jayson Hahn on 4/29/25. +// + +import Foundation +import GameAPI + +// Represents the type of games tab +enum GameTimeframe { + case past + case upcoming +} + +class GamesCacheManager { + + static let shared = GamesCacheManager() + + private var pastGames: [GamesQuery.Data.Game] = [] + private var upcomingGames: [GamesQuery.Data.Game] = [] + + // Keep track of pagination state + private var currentOffset = 0 + private var isLoading = false + private var hasMoreData = true + + private var allGameIds: Set = [] + + // Page size for each tab + private let defaultPageSize = 10 + + // Initialize with empty cache + private init() {} + + // Get cached games by type with specified limit + func getGames(type: GameTimeframe, limit: Int) -> [GamesQuery.Data.Game] { + switch type { + case .past: + return Array(pastGames.prefix(limit)) + case .upcoming: + return Array(upcomingGames.prefix(limit)) + } + } + + // Check if we have enough games of the specified type + func hasEnoughGames(type: GameTimeframe, count: Int) -> Bool { + switch type { + case .past: + return pastGames.count >= count + case .upcoming: + return upcomingGames.count >= count + } + } + + // TODO: Revise then when backend sorts games by date + // Load more games of the specified type + func loadMoreGames(type: GameTimeframe, pageSize: Int, completion: @escaping ([GamesQuery.Data.Game]?, Error?) -> Void) { + // If already loading or no more data, return + guard !isLoading && hasMoreData else { + completion([], nil) + return + } + + // If we already have enough games cached, just return from cache + if hasEnoughGames(type: type, count: getNextPageIndex(type: type) + pageSize) { + let startIndex = getNextPageIndex(type: type) + let endIndex = min(startIndex + pageSize, type == .past ? pastGames.count : upcomingGames.count) + let gamesPage = Array((type == .past ? pastGames : upcomingGames)[startIndex.. Int { + switch type { + case .past: + return pastGames.count + case .upcoming: + return upcomingGames.count + } + } + + // TODO: Revise then when backend sorts games by date + // Load more games from the network with increasing batch size until we have enough + private func loadMoreFromNetwork(desiredType: GameTimeframe, desiredCount: Int, completion: @escaping ([GamesQuery.Data.Game]?, Error?) -> Void) { + isLoading = true + + // Calculate how many more games we need - use a multiplier since we don't know how many will be of each type + let requestLimit = max(desiredCount * 3, 20) // Request more than needed to account for filtering + + NetworkManager.shared.fetchGames(limit: requestLimit, offset: currentOffset) { [weak self] games, error in + guard let self = self else { return } + + self.isLoading = false + + if let error = error { + completion(nil, error) + return + } + + guard let games = games else { + completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No games returned"])) + return + } + + // If no games returned, we've reached the end + if games.isEmpty { + self.hasMoreData = false + completion([], nil) + return + } + + // Sort games into past and upcoming categories using updated logic + let now = Date() + let twoHours: TimeInterval = 2 * 60 * 60 // For determining live games + let calendar = Calendar.current + let startOfToday = calendar.startOfDay(for: now) + + var newPastGames: [GamesQuery.Data.Game] = [] + var newUpcomingGames: [GamesQuery.Data.Game] = [] + + for game in games { + // TODO: make utc date not optional on backend + if let gameDate = self.parseDate(from: game.utcDate!) { + // Check if game is live (started but within 2 hours) + let isLive = gameDate < now && now.timeIntervalSince(gameDate) <= twoHours + + // Check if game is upcoming (hasn't started yet) + let isUpcoming = gameDate > now + + // Check if game finished today + let isFinishedToday = gameDate < now && gameDate >= startOfToday + + // Check if game finished before today + let isFinishedByToday = gameDate < startOfToday + + // Apply sorting logic + if isLive || isUpcoming || isFinishedToday { + // Live games should be at the beginning of upcoming games list + if isLive { + // Insert at beginning of upcoming games + newUpcomingGames.insert(game, at: 0) + } else { + // Regular upcoming or finished today games + newUpcomingGames.append(game) + } + } + + if isFinishedByToday { + newPastGames.append(game) + } + } + } + + // Update the cache + self.pastGames.append(contentsOf: newPastGames) + self.upcomingGames.append(contentsOf: newUpcomingGames) + + // Update the offset for the next request + self.currentOffset += games.count + + // Determine which games to return based on the requested type + var gamesForType: [GamesQuery.Data.Game] = [] + let startIndex = desiredType == .past ? self.pastGames.count - newPastGames.count : self.upcomingGames.count - newUpcomingGames.count + let allGamesOfType = desiredType == .past ? self.pastGames : self.upcomingGames + + if startIndex < allGamesOfType.count { + let endIndex = min(startIndex + desiredCount, allGamesOfType.count) + gamesForType = Array(allGamesOfType[startIndex.. Date? { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXX" + dateFormatter.timeZone = TimeZone(abbreviation: "UTC") + return dateFormatter.date(from: dateString) + } + + // Refresh the cache (e.g., on pull to refresh) + func refreshCache(completion: @escaping (Error?) -> Void) { + pastGames = [] + upcomingGames = [] + currentOffset = 0 + hasMoreData = true + + // Load initial data + NetworkManager.shared.fetchGames(limit: defaultPageSize * 2, offset: 0) { [weak self] games, error in + guard let self = self else { return } + + if let error = error { + completion(error) + return + } + + guard let games = games else { + completion(nil) + return + } + + // Sort games into past and upcoming + let now = Date() + for game in games { + if let gameDate = self.parseDate(from: game.date), gameDate < now { + self.pastGames.append(game) + } else { + self.upcomingGames.append(game) + } + } + + self.currentOffset = games.count + completion(nil) + } + } + +} diff --git a/score-ios/Networking/NetworkManager.swift b/score-ios/Networking/NetworkManager.swift index b00c8fe..57bace5 100644 --- a/score-ios/Networking/NetworkManager.swift +++ b/score-ios/Networking/NetworkManager.swift @@ -10,11 +10,12 @@ import Apollo import GameAPI class NetworkManager { + static let shared = NetworkManager() let apolloClient = ApolloClient(url: ScoreEnvironment.baseURL) - - func fetchGames(completion: @escaping ([GamesQuery.Data.Game]?, Error?) -> Void) { - apolloClient.fetch(query: GamesQuery()) { result in + + func fetchGames(limit: Int, offset: Int, completion: @escaping ([GamesQuery.Data.Game]?, Error?) -> Void) { + apolloClient.fetch(query: GamesQuery(limit: limit, offset: offset)) { result in switch result { case .success(let graphQLResult): if let gamesData = graphQLResult.data?.games?.compactMap({ $0 }) { @@ -28,10 +29,10 @@ class NetworkManager { } } } - + func fetchTeamById(by id: String, completion: @escaping (GetTeamByIdQuery.Data.Team?, Error?) -> Void) { let query = GetTeamByIdQuery(id: id) - + apolloClient.fetch(query: query) { result in switch result { case .success(let graphQLResult): @@ -45,5 +46,5 @@ class NetworkManager { } } } - + } diff --git a/score-ios/ViewModels/GamesViewModel.swift b/score-ios/ViewModels/GamesViewModel.swift index add5759..910b8e9 100644 --- a/score-ios/ViewModels/GamesViewModel.swift +++ b/score-ios/ViewModels/GamesViewModel.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import GameAPI // State enum to track the loading state enum DataState { @@ -23,6 +24,11 @@ class GamesViewModel: ObservableObject @Published var allUpcomingGames: [Game] = [] @Published var allPastGames: [Game] = [] + // private games data + private var privateGames: [Game] = [] + private var privateUpcomingGames: [Game] = [] + private var privatePastGames: [Game] = [] + // Carousel Logic @Published var selectedCardIndex: Int = 0 @Published var topUpcomingGames: [Game] = [] // Displayed in Carousel @@ -70,70 +76,226 @@ class GamesViewModel: ObservableObject } // Networking +// func fetchGames() { +// // Set loading state before fetch +// dataState = .loading +// +// NetworkManager.shared.fetchGames { fetchedGames, error in +// DispatchQueue.main.async { +// self.games.removeAll() +// self.allPastGames.removeAll() +// self.allUpcomingGames.removeAll() +// +// if let error = error { +// self.dataState = .error(error: .networkError) +// print("Error in fetchGames: \(error.localizedDescription)") +// return +// } +// +// guard let fetchedGames = fetchedGames else { +// self.dataState = .error(error: .emptyData) +// return +// } +// +// var updatedGames: [Game] = [] +// fetchedGames.indices.forEach { index in +// let gameData = fetchedGames[index] +// let game = Game(game: gameData) +// if Sport.allCases.contains(game.sport) && game.sport != Sport.All { +// // append the game only if it is upcoming/live +// let now = Date() +// let twoHours: TimeInterval = 2 * 60 * 60 // TODO: How to decide if a game is live +// let calendar = Calendar.current +// let startOfToday = calendar.startOfDay(for: now) +// +// let isLive = game.date < now && now.timeIntervalSince(game.date) <= twoHours +// let isUpcoming = game.date > now +// let isFinishedToday = game.date < now && game.date >= startOfToday +// let isFinishedByToday = game.date < startOfToday +// +// updatedGames.append(game) +// if isLive { +// self.allUpcomingGames.insert(game, at: 0) +// } else if isUpcoming { +// self.allUpcomingGames.append(game) +// } else if isFinishedToday { +// self.allUpcomingGames.append(game) +// } +// if isFinishedByToday { +// self.allPastGames.append(game) +// } +// } +// } +// // TODO: do this in a way that requires less copying by sorting at the top +// self.allPastGames.sort(by: {$0.date > $1.date}) +// self.allUpcomingGames.sort(by: {$0.date < $1.date}) +// self.games = updatedGames.sorted(by: {$0.date < $1.date}) +// self.topUpcomingGames = Array(self.allUpcomingGames.prefix(3)) +// self.topPastGames = Array(self.allPastGames.prefix(3)) +// self.filter() +// +// // Update state to success +// self.dataState = .success +// } +// } +// } + + // TODO: Remove once backend is has implemented pagination with sorted dates and pages by game type func fetchGames() { // Set loading state before fetch dataState = .loading + // Clear the current arrays + self.games.removeAll() + self.allPastGames.removeAll() + self.allUpcomingGames.removeAll() - NetworkManager.shared.fetchGames { fetchedGames, error in - DispatchQueue.main.async { - self.games.removeAll() - self.allPastGames.removeAll() - self.allUpcomingGames.removeAll() + // Start fetching from the first page + fetchGamesRecursively(limit: 50, offset: 0, accumulatedGames: []) + } - if let error = error { + private func fetchGamesRecursively(limit: Int, offset: Int, accumulatedGames: [GamesQuery.Data.Game], retryCount: Int = 0, maxRetries: Int = 3) { + // Create a timeout + let timeoutWorkItem = DispatchWorkItem { + print("Request timed out for offset: \(offset)") + // If we haven't exceeded max retries, try again + if retryCount < maxRetries { + print("Retrying request (\(retryCount + 1)/\(maxRetries))...") + DispatchQueue.main.async { + self.fetchGamesRecursively(limit: limit, offset: offset, accumulatedGames: accumulatedGames, retryCount: retryCount + 1, maxRetries: maxRetries) + } + } else { + print("Max retries exceeded. Giving up on request for offset: \(offset)") + DispatchQueue.main.async { +// // If this was the first request and it failed after all retries, show error +// if offset == 0 && accumulatedGames.isEmpty { +// self.dataState = .error(error: .networkError) +// } else { +// // Otherwise process what we have so far +// self.processGames(accumulatedGames) +// } self.dataState = .error(error: .networkError) - print("Error in fetchGames: \(error.localizedDescription)") - return } + } + } + + // Schedule timeout + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: timeoutWorkItem) + + NetworkManager.shared.fetchGames(limit: limit, offset: offset) { [weak self] fetchedGames, error in + guard let self = self else { return } + + // Cancel the timeout since we got a response + timeoutWorkItem.cancel() + + DispatchQueue.main.async { + if let error = error { + print("Error in fetchGames for offset \(offset): \(error.localizedDescription)") - guard let fetchedGames = fetchedGames else { - self.dataState = .error(error: .emptyData) + // If we haven't exceeded max retries, try again + if retryCount < maxRetries { + print("Retrying request (\(retryCount + 1)/\(maxRetries))...") + self.fetchGamesRecursively(limit: limit, offset: offset, accumulatedGames: accumulatedGames, retryCount: retryCount + 1, maxRetries: maxRetries) + } else { + print("Max retries exceeded. Giving up on request for offset: \(offset)") + // If this was the first request and it failed after all retries, show error + if offset == 0 && accumulatedGames.isEmpty { + self.dataState = .error(error: .networkError) + } else { + // Otherwise process what we have so far + self.processGames(accumulatedGames) + } + } return } - var updatedGames: [Game] = [] - fetchedGames.indices.forEach { index in - let gameData = fetchedGames[index] - let game = Game(game: gameData) - if Sport.allCases.contains(game.sport) && game.sport != Sport.All { - // append the game only if it is upcoming/live - let now = Date() - let twoHours: TimeInterval = 2 * 60 * 60 // TODO: How to decide if a game is live - let calendar = Calendar.current - let startOfToday = calendar.startOfDay(for: now) - - let isLive = game.date < now && now.timeIntervalSince(game.date) <= twoHours - let isUpcoming = game.date > now - let isFinishedToday = game.date < now && game.date >= startOfToday - let isFinishedByToday = game.date < startOfToday - - updatedGames.append(game) - if isLive { - self.allUpcomingGames.insert(game, at: 0) - } else if isUpcoming { - self.allUpcomingGames.append(game) - } else if isFinishedToday { - self.allUpcomingGames.append(game) - } - if isFinishedByToday { - self.allPastGames.append(game) - } + guard let fetchedGames = fetchedGames, !fetchedGames.isEmpty else { + // If this is the first fetch and no games, show empty data error + if offset == 0 { + self.dataState = .error(error: .emptyData) + } else { +// // Otherwise process all accumulated games +// self.processGames(accumulatedGames) + self.dataState = .error(error: .networkError) } + return + } + + // Combine newly fetched games with previously accumulated games + let allGames = accumulatedGames + fetchedGames + + // If we received a full page, there might be more games to fetch + if fetchedGames.count == limit { + // Continue fetching the next page (reset retry count for the next page) + self.fetchGamesRecursively(limit: limit, offset: offset + limit, accumulatedGames: allGames, retryCount: 0, maxRetries: maxRetries) + } else { + // We've fetched all games, process them + self.processGames(allGames) } - // TODO: do this in a way that requires less copying by sorting at the top - self.allPastGames.sort(by: {$0.date > $1.date}) - self.allUpcomingGames.sort(by: {$0.date < $1.date}) - self.games = updatedGames.sorted(by: {$0.date < $1.date}) - self.topUpcomingGames = Array(self.allUpcomingGames.prefix(3)) - self.topPastGames = Array(self.allPastGames.suffix(3)) - self.filter() - - // Update state to success - self.dataState = .success } } } + private func processGames(_ gameDataArray: [GamesQuery.Data.Game]) { + var updatedGames: [Game] = [] + gameDataArray.indices.forEach { index in + let gameData = gameDataArray[index] + let game = Game(game: gameData) + if Sport.allCases.contains(game.sport) && game.sport != Sport.All { + // append the game only if it is upcoming/live + let now = Date() + let twoHours: TimeInterval = 2 * 60 * 60 // TODO: How to decide if a game is live + let calendar = Calendar.current + let startOfToday = calendar.startOfDay(for: now) + let isLive = game.date < now && now.timeIntervalSince(game.date) <= twoHours + let isUpcoming = game.date > now + let isFinishedToday = game.date < now && game.date >= startOfToday + let isFinishedByToday = game.date < startOfToday + updatedGames.append(game) + if isLive { + self.privateUpcomingGames.insert(game, at: 0) + } else if isUpcoming { + self.privateUpcomingGames.append(game) + } else if isFinishedToday { + self.privateUpcomingGames.append(game) + } + if isFinishedByToday { + self.privatePastGames.append(game) + } + } + } + + // Filter out duplicates before sorting + self.allPastGames = uniqueGames(from: self.privatePastGames) + self.allUpcomingGames = uniqueGames(from: self.privateUpcomingGames) + self.games = uniqueGames(from: updatedGames) + + // Sort all the collections + self.allPastGames.sort(by: {$0.date > $1.date}) + self.allUpcomingGames.sort(by: {$0.date < $1.date}) + self.games = updatedGames.sorted(by: {$0.date < $1.date}) + self.topUpcomingGames = Array(self.allUpcomingGames.prefix(3)) + self.topPastGames = Array(self.allPastGames.prefix(3)) + self.filter() + + // Update state to success + self.dataState = .success + } + + // Function to filter out duplicate games by ID + func uniqueGames(from games: [Game]) -> [Game] { + var uniqueGames: [Game] = [] + var seenIDs: Set = [] + + for game in games { + if let id = game.serverId, !seenIDs.contains(id) { + uniqueGames.append(game) + seenIDs.insert(id) + } + } + + return uniqueGames + } + // Method to retry after an error func retryFetch() { fetchGames() diff --git a/score-ios/ViewModels/PastGameViewModel.swift b/score-ios/ViewModels/PastGameViewModel.swift index 7532499..aeb8dcf 100644 --- a/score-ios/ViewModels/PastGameViewModel.swift +++ b/score-ios/ViewModels/PastGameViewModel.swift @@ -13,42 +13,56 @@ class PastGameViewModel: ObservableObject { var numberOfRounds: Int { switch game.sport { - case .Baseball: return 9 + case .Baseball: return game.timeUpdates.count > 3 ? game.timeUpdates.count - 3 : 9 + // number of innings is not always 9 case .Soccer: return 2 - case .IceHockey: return 3 +// case .IceHockey: return 3 case .FieldHockey, .Football, .Lacrosse: return 4 default: return 1 } } - // TODO: will be discarded once backend is changed to include a total score in scoreBreakdown for all sports - var cornellTotalScore: Int { - if game.timeUpdates.count == numberOfRounds { - return game.timeUpdates.reduce(0, { $0 + $1.cornellScore }) // sum up the score for each round - } else if game.timeUpdates.count == numberOfRounds + 1 { - // the last one is the sum - return game.timeUpdates[game.timeUpdates.count-1].cornellScore - } else if game.timeUpdates.count > numberOfRounds { - let scores = game.timeUpdates[0.. 3 ? game.timeUpdates.count - 2 : 10 + // the last three columns are total runs, hits, and errors + // if backend stores null for scoreBreakdown, display regular score box with 10 columns + case .Soccer: return game.timeUpdates.count >= 3 ? game.timeUpdates.count : 3 +// case .IceHockey: return game.timeUpdates.count + case .FieldHockey, .Football, .Lacrosse: return game.timeUpdates.count >= 5 ? game.timeUpdates.count : 5 + default: return 1 + } + } + + var numberOfOvertimes: Int { + switch game.sport { + case .Baseball: return -1 + case .Soccer: return game.timeUpdates.count - 3 +// case .IceHockey: return game.timeUpdates.count - 4 + case .FieldHockey, .Football, .Lacrosse: return game.timeUpdates.count - 5 + default: return -1 } - else { - return -1 + } + + + var cornellTotalScore: Int { + // TODO: Get this back when backend fixes the boxScore (make sure the last entry reflects total score correctly) +// return game.gameUpdates.count > 0 ? game.gameUpdates[game.gameUpdates.count - 1].cornellScore : -1 + if game.sport == .Baseball { + return game.gameUpdates.count > 0 ? game.gameUpdates[game.gameUpdates.count - 1].cornellScore : -1 } + + return game.timeUpdates.count > 0 ? game.timeUpdates[game.timeUpdates.count - 1].cornellScore : -1 } var opponentTotalScore: Int { - if game.timeUpdates.count == numberOfRounds { - return game.timeUpdates.reduce(0, { $0 + $1.opponentScore }) - } else if game.timeUpdates.count == numberOfRounds + 1 { - // the last one is the sum - return game.timeUpdates[game.timeUpdates.count-1].opponentScore - } else if game.timeUpdates.count > numberOfRounds { - let scores = game.timeUpdates[0.. 0 ? game.gameUpdates[game.gameUpdates.count - 1].opponentScore : -1 + if game.sport == .Baseball { + return game.gameUpdates.count > 0 ? game.gameUpdates[game.gameUpdates.count - 1].opponentScore : -1 } + + return game.timeUpdates.count > 0 ? game.timeUpdates[game.timeUpdates.count - 1].opponentScore : -1 } var corScore: String { @@ -62,5 +76,4 @@ class PastGameViewModel: ObservableObject { init(game: Game) { self.game = game } - } diff --git a/score-ios/Views/DetailedViews/DynamicScoreBox.swift b/score-ios/Views/DetailedViews/DynamicScoreBox.swift index a0b8e67..abfe92b 100644 --- a/score-ios/Views/DetailedViews/DynamicScoreBox.swift +++ b/score-ios/Views/DetailedViews/DynamicScoreBox.swift @@ -15,7 +15,7 @@ struct DynamicScoreBox: View { GeometryReader { geometry in let boxWidth = geometry.size.width let firstColWidth = boxWidth / 5 - let columnWidth = (boxWidth - firstColWidth) / CGFloat((viewModel.numberOfRounds + 1)) + let columnWidth = (boxWidth - firstColWidth) / CGFloat(viewModel.numberOfColumns) VStack(spacing: 0) { firstRow(firstColWidth: firstColWidth, columnWidth: columnWidth) @@ -54,6 +54,14 @@ extension DynamicScoreBox { } .font(Constants.Fonts.gameText) + if (viewModel.numberOfOvertimes >= 1) { + ForEach(1...viewModel.numberOfOvertimes, id: \..self) { ot in + Text("OT \(ot)") + .frame(width: columnWidth) + } + .font(Constants.Fonts.gameText) + } + Text("Total") .font(Constants.Fonts.gameText) .padding(.trailing, 3) @@ -71,7 +79,7 @@ extension DynamicScoreBox { .padding(.leading, 5) .frame(width: firstColWidth, alignment: .leading) - ForEach(0..: View { - + let games: [Game] let tileView: (Game) -> TileView - + + @State private var displayedGamesCount = 10 + var body: some View { LazyVStack(spacing: 16) { if games.isEmpty { NoGameView() .frame(maxWidth: .infinity, maxHeight: .infinity) } else { - ForEach(games) { game in + // Calculate how many games to show (either displayedGamesCount or all games if less) + let gamesToShow = min(displayedGamesCount, games.count) + + // Only display the subset of games + ForEach(games.prefix(gamesToShow)) { game in GeometryReader { cellGeometry in let isCellCovered = cellGeometry.frame(in: .global).minY < 100 if !isCellCovered { @@ -31,8 +37,27 @@ struct GameListView: View { .buttonStyle(PlainButtonStyle()) } } + .frame(height: 96) + } + + // Load More Button - only show if we haven't displayed all games yet + if displayedGamesCount < games.count { + Button(action: { + // Increase the displayed games count by 10 more (or remaining count) + displayedGamesCount = min(displayedGamesCount + 10, games.count) + }) { + Text("Load More") + .font(Constants.Fonts.buttonLabel) + .foregroundStyle(Constants.Colors.white) +// .frame(maxWidth: .infinity) + .padding() + .background(Constants.Colors.primary_red) + .cornerRadius(8) + } + .clipShape(Capsule()) + .padding(.top, 8) + .padding(.horizontal, 20) } - .frame(height: 96) // Temp Fix So the last cell is not covered by the tab bar Rectangle()