diff --git a/.DS_Store b/.DS_Store index 3e9981a..bb67f21 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/apollo-codegen-config.json b/apollo-codegen-config.json new file mode 100644 index 0000000..b85ec7e --- /dev/null +++ b/apollo-codegen-config.json @@ -0,0 +1,43 @@ +{ + "schemaNamespace" : "gameAPI", + "input" : { + "operationSearchPaths" : [ + "**/*.graphql" + ], + "schemaSearchPaths" : [ + "**/*.graphqls" + ] + }, + "schemaDownloadConfiguration": { + "downloadMethod": { + "introspection": { + "endpointURL": "http://localhost:5000/graphql", + "httpMethod": { + "POST": {} + }, + "includeDeprecatedInputValues": false, + "outputFormat": "SDL" + } + }, + "downloadTimeout": 60, + "headers": [], + "outputPath": "./score-ios/Models/GraphQL/schema.graphqls" + }, + "output" : { + "testMocks" : { + "none" : { + } + }, + "schemaTypes" : { + "path" : "./gameAPI", + "moduleType" : { + "swiftPackageManager" : { + } + } + }, + "operations" : { + "inSchemaModule" : { + } + } + } +} diff --git a/apollo-ios-cli b/apollo-ios-cli new file mode 100755 index 0000000..dbfbcdf Binary files /dev/null and b/apollo-ios-cli differ diff --git a/gameAPI/.DS_Store b/gameAPI/.DS_Store new file mode 100644 index 0000000..4d3e227 Binary files /dev/null and b/gameAPI/.DS_Store differ diff --git a/gameAPI/Package.swift b/gameAPI/Package.swift new file mode 100644 index 0000000..141f37d --- /dev/null +++ b/gameAPI/Package.swift @@ -0,0 +1,28 @@ +// 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 new file mode 100644 index 0000000..8de1a40 Binary files /dev/null and b/gameAPI/Sources/.DS_Store differ diff --git a/gameAPI/Sources/Operations/.DS_Store b/gameAPI/Sources/Operations/.DS_Store new file mode 100644 index 0000000..4ffd9d2 Binary files /dev/null and b/gameAPI/Sources/Operations/.DS_Store differ diff --git a/gameAPI/Sources/Operations/Queries/GamesQuery.graphql.swift b/gameAPI/Sources/Operations/Queries/GamesQuery.graphql.swift new file mode 100644 index 0000000..c77a8a7 --- /dev/null +++ b/gameAPI/Sources/Operations/Queries/GamesQuery.graphql.swift @@ -0,0 +1,96 @@ +// @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 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("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 boxScore: [BoxScore?]? { __data["boxScore"] } + + /// 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 new file mode 100644 index 0000000..b8b7fc2 --- /dev/null +++ b/gameAPI/Sources/Operations/Queries/GetTeamByIdQuery.graphql.swift @@ -0,0 +1,54 @@ +// @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 new file mode 100644 index 0000000..a4ae68e Binary files /dev/null and b/gameAPI/Sources/Schema/.DS_Store differ diff --git a/gameAPI/Sources/Schema/Objects/BoxScore.graphql.swift b/gameAPI/Sources/Schema/Objects/BoxScore.graphql.swift new file mode 100644 index 0000000..02aa898 --- /dev/null +++ b/gameAPI/Sources/Schema/Objects/BoxScore.graphql.swift @@ -0,0 +1,14 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// A GraphQL type representing a boxscore. + /// + /// Attributes: + static let BoxScore = ApolloAPI.Object( + typename: "BoxScore", + 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 new file mode 100644 index 0000000..4e32429 --- /dev/null +++ b/gameAPI/Sources/Schema/Objects/GameType.graphql.swift @@ -0,0 +1,23 @@ +// @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. + /// - `opponent_id`: The id of the opposing team. + /// - `sport`: The sport of the game. + /// - `state`: The state of the game. + /// - `time`: The time 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 new file mode 100644 index 0000000..a4155e4 --- /dev/null +++ b/gameAPI/Sources/Schema/Objects/Query.graphql.swift @@ -0,0 +1,11 @@ +// @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 new file mode 100644 index 0000000..e714216 --- /dev/null +++ b/gameAPI/Sources/Schema/Objects/TeamType.graphql.swift @@ -0,0 +1,18 @@ +// @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 new file mode 100644 index 0000000..8723501 --- /dev/null +++ b/gameAPI/Sources/Schema/SchemaConfiguration.swift @@ -0,0 +1,15 @@ +// @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 new file mode 100644 index 0000000..4805c9e --- /dev/null +++ b/gameAPI/Sources/Schema/SchemaMetadata.graphql.swift @@ -0,0 +1,34 @@ +// @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 97ecc42..98bf801 100644 --- a/score-ios.xcodeproj/project.pbxproj +++ b/score-ios.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -11,6 +11,8 @@ CE335CD52C922ECB0037F572 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE335CD42C922ECB0037F572 /* Constants.swift */; }; CE335CD72C922F390037F572 /* Dates.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE335CD62C922F390037F572 /* Dates.swift */; }; CE335CD92C9244230037F572 /* Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE335CD82C9244230037F572 /* Game.swift */; }; + CE3C9C412D010177008BFB4C /* ScoringSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3C9C402D010177008BFB4C /* ScoringSummary.swift */; }; + CE3C9C432D011A23008BFB4C /* OrdinalSuffix.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3C9C422D011A23008BFB4C /* OrdinalSuffix.swift */; }; CE528FA02C96420700C238B5 /* PickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE528F9F2C96420700C238B5 /* PickerView.swift */; }; CE528FA22C9651C800C238B5 /* GameTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE528FA12C9651C800C238B5 /* GameTile.swift */; }; CE528FA42C9653C200C238B5 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE528FA32C9653C200C238B5 /* Error.swift */; }; @@ -44,10 +46,22 @@ CE725D592C89120500386943 /* score_iosUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE725D582C89120500386943 /* score_iosUITestsLaunchTests.swift */; }; D836AD922CB62C8800BD1545 /* NoGameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D836AD912CB62C8800BD1545 /* NoGameView.swift */; }; D83EE8862CC9917C008B693C /* ScoreSummaryTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83EE8852CC9917C008B693C /* ScoreSummaryTile.swift */; }; + D86347AF2CDBD2F4003DD8F6 /* PastGameCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86347AE2CDBD2F3003DD8F6 /* PastGameCard.swift */; }; + D86347B12CDBFF7C003DD8F6 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86347B02CDBFF7C003DD8F6 /* HomeView.swift */; }; + D86347DF2CE98B3C003DD8F6 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86347DE2CE98B3C003DD8F6 /* MainTabView.swift */; }; + D86347E12CE98D37003DD8F6 /* TabViewIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86347E02CE98D37003DD8F6 /* TabViewIcon.swift */; }; + D87787C82CFFAE5A00EA79E1 /* GamesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87787C72CFFAE5200EA79E1 /* GamesViewModel.swift */; }; D87882282CC060FC00421F67 /* GameDetailedScoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87882272CC060FC00421F67 /* GameDetailedScoreView.swift */; }; + D89102042CED69EF004CE226 /* Apollo in Frameworks */ = {isa = PBXBuildFile; productRef = D89102032CED69EF004CE226 /* Apollo */; }; + D89102072CED6A39004CE226 /* schema.graphqls in Resources */ = {isa = PBXBuildFile; fileRef = D89102062CED6A28004CE226 /* schema.graphqls */; }; + D891020B2CED6A8E004CE226 /* Game.graphql in Resources */ = {isa = PBXBuildFile; fileRef = D891020A2CED6A86004CE226 /* Game.graphql */; }; + D89102102CF0EBA4004CE226 /* DataCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D891020F2CF0EBA0004CE226 /* DataCoordinator.swift */; }; + D89102132CF10CA9004CE226 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89102122CF10CA5004CE226 /* NetworkManager.swift */; }; + D89102182CF151DD004CE226 /* GameAPI in Frameworks */ = {isa = PBXBuildFile; productRef = D89102172CF151DD004CE226 /* GameAPI */; }; D89461192CBF393B0010C532 /* UpcomingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89461182CBF393B0010C532 /* UpcomingCard.swift */; }; D8B1C9D12CD2CE3D0095E563 /* PastGameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B1C9D02CD2CE3C0095E563 /* PastGameView.swift */; }; D8B1C9D32CD2D20A0095E563 /* PastGameTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B1C9D22CD2D20A0095E563 /* PastGameTile.swift */; }; + D8DD4E642CFD48ED00F2C46E /* Team.graphql in Resources */ = {isa = PBXBuildFile; fileRef = D8DD4E632CFD48E400F2C46E /* Team.graphql */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -72,6 +86,8 @@ CE335CD42C922ECB0037F572 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; CE335CD62C922F390037F572 /* Dates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dates.swift; sourceTree = ""; }; CE335CD82C9244230037F572 /* Game.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Game.swift; sourceTree = ""; }; + CE3C9C402D010177008BFB4C /* ScoringSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoringSummary.swift; sourceTree = ""; }; + CE3C9C422D011A23008BFB4C /* OrdinalSuffix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrdinalSuffix.swift; sourceTree = ""; }; CE528F9F2C96420700C238B5 /* PickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerView.swift; sourceTree = ""; }; CE528FA12C9651C800C238B5 /* GameTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameTile.swift; sourceTree = ""; }; CE528FA32C9653C200C238B5 /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; @@ -109,10 +125,20 @@ CE725D582C89120500386943 /* score_iosUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = score_iosUITestsLaunchTests.swift; sourceTree = ""; }; D836AD912CB62C8800BD1545 /* NoGameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoGameView.swift; sourceTree = ""; }; D83EE8852CC9917C008B693C /* ScoreSummaryTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoreSummaryTile.swift; sourceTree = ""; }; + D86347AE2CDBD2F3003DD8F6 /* PastGameCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PastGameCard.swift; sourceTree = ""; }; + D86347B02CDBFF7C003DD8F6 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + D86347DE2CE98B3C003DD8F6 /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; + D86347E02CE98D37003DD8F6 /* TabViewIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewIcon.swift; sourceTree = ""; }; + D87787C72CFFAE5200EA79E1 /* GamesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GamesViewModel.swift; sourceTree = ""; }; D87882272CC060FC00421F67 /* GameDetailedScoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameDetailedScoreView.swift; sourceTree = ""; }; + D89102062CED6A28004CE226 /* schema.graphqls */ = {isa = PBXFileReference; lastKnownFileType = text; path = schema.graphqls; sourceTree = ""; }; + D891020A2CED6A86004CE226 /* Game.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = Game.graphql; sourceTree = ""; }; + D891020F2CF0EBA0004CE226 /* DataCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCoordinator.swift; sourceTree = ""; }; + D89102122CF10CA5004CE226 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; D89461182CBF393B0010C532 /* UpcomingCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpcomingCard.swift; sourceTree = ""; }; D8B1C9D02CD2CE3C0095E563 /* PastGameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PastGameView.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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -120,6 +146,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D89102042CED69EF004CE226 /* Apollo in Frameworks */, + D89102182CF151DD004CE226 /* GameAPI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -158,6 +186,7 @@ CE335CCC2C9226F90037F572 /* Models */ = { isa = PBXGroup; children = ( + D89102052CED6A1E004CE226 /* GraphQL */, CE335CD82C9244230037F572 /* Game.swift */, ); path = Models; @@ -179,6 +208,7 @@ CE335CD42C922ECB0037F572 /* Constants.swift */, CE335CD62C922F390037F572 /* Dates.swift */, CE528FA32C9653C200C238B5 /* Error.swift */, + CE3C9C422D011A23008BFB4C /* OrdinalSuffix.swift */, ); path = Utils; sourceTree = ""; @@ -204,6 +234,11 @@ D83EE8852CC9917C008B693C /* ScoreSummaryTile.swift */, D8B1C9D02CD2CE3C0095E563 /* PastGameView.swift */, D8B1C9D22CD2D20A0095E563 /* PastGameTile.swift */, + D86347AE2CDBD2F3003DD8F6 /* PastGameCard.swift */, + D86347B02CDBFF7C003DD8F6 /* HomeView.swift */, + D86347DE2CE98B3C003DD8F6 /* MainTabView.swift */, + D86347E02CE98D37003DD8F6 /* TabViewIcon.swift */, + CE3C9C402D010177008BFB4C /* ScoringSummary.swift */, ); path = Views; sourceTree = ""; @@ -240,6 +275,7 @@ CE725D3A2C89120200386943 /* score-ios */, CE725D4B2C89120500386943 /* score-iosTests */, CE725D552C89120500386943 /* score-iosUITests */, + D89102022CED69EF004CE226 /* Frameworks */, CE725D392C89120200386943 /* Products */, ); sourceTree = ""; @@ -257,6 +293,8 @@ CE725D3A2C89120200386943 /* score-ios */ = { isa = PBXGroup; children = ( + D87787C62CFFAE3D00EA79E1 /* ViewModels */, + D891020D2CF0EB88004CE226 /* Data */, CE528FF32C96A50F00C238B5 /* Info.plist */, CE725D412C89120400386943 /* Preview Content */, CE335CD12C9227170037F572 /* Views */, @@ -295,6 +333,40 @@ path = "score-iosUITests"; sourceTree = ""; }; + D87787C62CFFAE3D00EA79E1 /* ViewModels */ = { + isa = PBXGroup; + children = ( + D87787C72CFFAE5200EA79E1 /* GamesViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + D89102022CED69EF004CE226 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + D89102052CED6A1E004CE226 /* GraphQL */ = { + isa = PBXGroup; + children = ( + D8DD4E632CFD48E400F2C46E /* Team.graphql */, + D891020A2CED6A86004CE226 /* Game.graphql */, + D89102062CED6A28004CE226 /* schema.graphqls */, + ); + path = GraphQL; + sourceTree = ""; + }; + D891020D2CF0EB88004CE226 /* Data */ = { + isa = PBXGroup; + children = ( + D89102122CF10CA5004CE226 /* NetworkManager.swift */, + D891020F2CF0EBA0004CE226 /* DataCoordinator.swift */, + ); + path = Data; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -302,6 +374,7 @@ isa = PBXNativeTarget; buildConfigurationList = CE725D5C2C89120500386943 /* Build configuration list for PBXNativeTarget "score-ios" */; buildPhases = ( + D89102112CF0EDDF004CE226 /* Generate Apollo GraphQL API */, CE725D342C89120200386943 /* Sources */, CE725D352C89120200386943 /* Frameworks */, CE725D362C89120200386943 /* Resources */, @@ -383,6 +456,10 @@ Base, ); mainGroup = CE725D2F2C89120100386943; + packageReferences = ( + D89102012CED68D9004CE226 /* XCRemoteSwiftPackageReference "apollo-ios" */, + D89102162CF151DD004CE226 /* XCLocalSwiftPackageReference "gameAPI" */, + ); productRefGroup = CE725D392C89120200386943 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -399,6 +476,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D8DD4E642CFD48ED00F2C46E /* Team.graphql in Resources */, + D891020B2CED6A8E004CE226 /* Game.graphql in Resources */, CE528FE42C96A27500C238B5 /* Poppins-Light.ttf in Resources */, CE528FEE2C96A27500C238B5 /* Poppins-Black.ttf in Resources */, CE528FEC2C96A27500C238B5 /* Poppins-LightItalic.ttf in Resources */, @@ -409,6 +488,7 @@ CE528FE12C96A27500C238B5 /* Poppins-ThinItalic.ttf in Resources */, CE528FE22C96A27500C238B5 /* Poppins-ExtraLightItalic.ttf in Resources */, CE528FE92C96A27500C238B5 /* Poppins-BlackItalic.ttf in Resources */, + D89102072CED6A39004CE226 /* schema.graphqls in Resources */, CE528FE72C96A27500C238B5 /* Poppins-ExtraBoldItalic.ttf in Resources */, CE528FE32C96A27500C238B5 /* Poppins-BoldItalic.ttf in Resources */, CE528FE62C96A27500C238B5 /* Poppins-SemiBoldItalic.ttf in Resources */, @@ -439,18 +519,44 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + D89102112CF0EDDF004CE226 /* Generate Apollo GraphQL API */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Generate Apollo GraphQL API"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = ""; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ CE725D342C89120200386943 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CE3C9C432D011A23008BFB4C /* OrdinalSuffix.swift in Sources */, + D89102132CF10CA9004CE226 /* NetworkManager.swift in Sources */, CE725D3E2C89120200386943 /* ScoreApp.swift in Sources */, D836AD922CB62C8800BD1545 /* NoGameView.swift in Sources */, CE528FF52C9798AC00C238B5 /* FilterTile.swift in Sources */, D8B1C9D32CD2D20A0095E563 /* PastGameTile.swift in Sources */, CE528FA22C9651C800C238B5 /* GameTile.swift in Sources */, CE335CD92C9244230037F572 /* Game.swift in Sources */, + D89102102CF0EBA4004CE226 /* DataCoordinator.swift in Sources */, + D87787C82CFFAE5A00EA79E1 /* GamesViewModel.swift in Sources */, CE335CD72C922F390037F572 /* Dates.swift in Sources */, + D86347AF2CDBD2F4003DD8F6 /* PastGameCard.swift in Sources */, CE528FF72C979DA000C238B5 /* GameView.swift in Sources */, D87882282CC060FC00421F67 /* GameDetailedScoreView.swift in Sources */, D83EE8862CC9917C008B693C /* ScoreSummaryTile.swift in Sources */, @@ -461,6 +567,10 @@ CE528FA02C96420700C238B5 /* PickerView.swift in Sources */, CE528FA42C9653C200C238B5 /* Error.swift in Sources */, CE335CD52C922ECB0037F572 /* Constants.swift in Sources */, + D86347E12CE98D37003DD8F6 /* TabViewIcon.swift in Sources */, + CE3C9C412D010177008BFB4C /* ScoringSummary.swift in Sources */, + D86347B12CDBFF7C003DD8F6 /* HomeView.swift in Sources */, + D86347DF2CE98B3C003DD8F6 /* MainTabView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -624,7 +734,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"score-ios/Preview Content\""; - DEVELOPMENT_TEAM = 9VB7SD8G8W; + DEVELOPMENT_TEAM = YLRVJFG2A8; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "score-ios/Info.plist"; @@ -654,7 +764,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"score-ios/Preview Content\""; - DEVELOPMENT_TEAM = 9VB7SD8G8W; + DEVELOPMENT_TEAM = YLRVJFG2A8; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "score-ios/Info.plist"; @@ -792,6 +902,36 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + D89102162CF151DD004CE226 /* XCLocalSwiftPackageReference "gameAPI" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = gameAPI; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCRemoteSwiftPackageReference section */ + D89102012CED68D9004CE226 /* XCRemoteSwiftPackageReference "apollo-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apollographql/apollo-ios.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.15.3; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + D89102032CED69EF004CE226 /* Apollo */ = { + isa = XCSwiftPackageProductDependency; + package = D89102012CED68D9004CE226 /* XCRemoteSwiftPackageReference "apollo-ios" */; + productName = Apollo; + }; + D89102172CF151DD004CE226 /* GameAPI */ = { + isa = XCSwiftPackageProductDependency; + productName = GameAPI; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = CE725D302C89120100386943 /* Project object */; } diff --git a/score-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/score-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..092041f --- /dev/null +++ b/score-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "apollo-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apollographql/apollo-ios.git", + "state" : { + "revision" : "c3f48d45ec1300bc95243bf19f67284f9dc0d14a", + "version" : "1.15.3" + } + }, + { + "identity" : "sqlite.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stephencelis/SQLite.swift.git", + "state" : { + "revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8", + "version" : "0.15.3" + } + } + ], + "version" : 2 +} diff --git a/score-ios.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/score-ios.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/score-ios.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/score-ios/.DS_Store b/score-ios/.DS_Store index 6674210..4e1c782 100644 Binary files a/score-ios/.DS_Store and b/score-ios/.DS_Store differ diff --git a/score-ios/Data/DataCoordinator.swift b/score-ios/Data/DataCoordinator.swift new file mode 100644 index 0000000..837e9d8 --- /dev/null +++ b/score-ios/Data/DataCoordinator.swift @@ -0,0 +1,10 @@ +// +// DataCoordinator.swift +// score-ios +// +// Created by Hsia Lu wu on 11/22/24. +// +import Foundation +import Apollo + +let apolloClient = ApolloClient(url: URL(string: "http://localhost:5000/graphql")!) diff --git a/score-ios/Data/NetworkManager.swift b/score-ios/Data/NetworkManager.swift new file mode 100644 index 0000000..2b9f8f7 --- /dev/null +++ b/score-ios/Data/NetworkManager.swift @@ -0,0 +1,79 @@ +// +// NetworkManager.swift +// score-ios +// +// Created by Hsia Lu wu on 11/22/24. +// +import Foundation +import SwiftUI +import Apollo +import GameAPI + +class NetworkManager { + static let shared = NetworkManager() + let apolloClient = ApolloClient(url: URL(string: "http://localhost:5000/graphql")!) + + // private(set) lazy var apollo: ApolloClient = { +// let url = URL(string: "https://yourgraphqlendpoint.com/graphql")! +// return ApolloClient(url: url) +//}() + + func fetchGames(completion: @escaping ([GamesQuery.Data.Game]?, Error?) -> Void) { + apolloClient.fetch(query: GamesQuery()) { result in + switch result { + case .success(let graphQLResult): + if let gamesData = graphQLResult.data?.games?.compactMap({ $0 }) { + completion(gamesData, nil) + } else if let errors = graphQLResult.errors { + let errorDescription = errors.map { $0.localizedDescription }.joined(separator: "\n") + completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: errorDescription])) + } + case .failure(let error): + completion(nil, error) + } + } + } + + func filterUpcomingGames(gender: String?, sport: String?, completion: @escaping ([GamesQuery.Data.Game]?, Error?) -> Void) { + apolloClient.fetch(query: GamesQuery()) { result in + switch result { + case .success(let graphQLResult): + if let gamesData = graphQLResult.data?.games?.compactMap({ $0 }) { + // filter games by gender and sports +// for datum in gamesData { +// print("Game of \(datum.sport) and \(datum.gender)") +// } + let filteredGames = gamesData.filter { game in + (gender == nil || game.gender == gender) && + (sport == nil || game.sport == sport) + } +// print("game is empty: " + String(filteredGames.isEmpty)) + completion(filteredGames, nil) + } else if let errors = graphQLResult.errors { + let errorDescription = errors.map { $0.localizedDescription }.joined(separator: "\n") + completion(nil, NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: errorDescription])) + } + case .failure(let error): + completion(nil, error) + } + } + } + + 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): + if let team = graphQLResult.data?.team { + completion(team, nil) + } else if let errors = graphQLResult.errors { + completion(nil, errors.first!) + } + case .failure(let error): + completion(nil, error) + } + } + } + +} diff --git a/score-ios/Models/.DS_Store b/score-ios/Models/.DS_Store new file mode 100644 index 0000000..0134d33 Binary files /dev/null and b/score-ios/Models/.DS_Store differ diff --git a/score-ios/Models/Game.swift b/score-ios/Models/Game.swift index 66a3daf..e4fec27 100644 --- a/score-ios/Models/Game.swift +++ b/score-ios/Models/Game.swift @@ -6,10 +6,11 @@ // import SwiftUI +import GameAPI protocol GameType : Identifiable where ID == UUID { // On Card and Details - var opponent: Opponent { get } + var opponent: Team { get } var city: String { get } var state: String { get } var date: Date { get } @@ -21,18 +22,297 @@ protocol GameType : Identifiable where ID == UUID { var address: String { get } // TODO: game score details - + + var timeUpdates: [TimeUpdate] { get } + var gameUpdates: [GameUpdate] { get } } -struct Game : GameType { +struct Game : GameType, Identifiable { var id: UUID = UUID() - var opponent: Opponent + var opponent: Team var city: String var state: String var date: Date var sport: Sport var address: String var sex: Sex + var timeUpdates: [TimeUpdate] = [] + var gameUpdates: [GameUpdate] = [] + + enum CodingKeys: String, CodingKey { + case id, city, state, date, sport, gender, location, opponentId, result, time, scoreBreakdown, boxScore + } + + init(game: GamesQuery.Data.Game) { + self.city = game.city + self.state = game.state + self.date = Game.parseDate(dateString: game.date, timeString: game.time ?? "12:00 p.m.") + self.sex = game.gender == "Mens" ? .Men : .Women +// self.sport = Sport(rawValue: game.sport) ?? .All + self.sport = Sport(normalizedValue: game.sport) ?? .All + self.opponent = Team.defaultTeam() + self.address = game.location ?? "N/A" + self.timeUpdates = parseScoreBreakdown(game.scoreBreakdown) + self.gameUpdates = parseBoxScore(decodeBoxScoreArray(boxScores: game.boxScore)) + } + + // initializer with provided arguments + init( + opponent: Team, + city: String, + state: String, + date: Date, + sport: Sport, + address: String?, + sex: Sex, + timeUpdates: [TimeUpdate], + gameUpdates: [GameUpdate] + ) { + self.opponent = opponent + self.city = city + self.state = state + self.date = date + self.sport = sport + self.address = address ?? "" + self.sex = sex + self.timeUpdates = timeUpdates + self.gameUpdates = gameUpdates + } + +} + +extension Game { + // parse score breakdown into TimeUpdate + mutating func parseScoreBreakdown(_ breakdown: [[String?]?]?) -> [TimeUpdate] { + var updates: [TimeUpdate] = [] + // Parse breakdown and map into `TimeUpdate` array + // [["1", "2"], ["2", "3"]] + + if (breakdown != nil) { + let scoreBreakDown = breakdown! + let corScores = scoreBreakDown[0] + let oppScores = scoreBreakDown[1] + var corTotal = 0 + var oppTotal = 0 + if (corScores != nil && oppScores != nil) { + corScores!.indices.forEach({ index in + let timeStamp = index+1 + if (corScores![index] != nil && oppScores![index] != nil) { + let corScore = Int(corScores![index]!) ?? 0 + let oppScore = Int(oppScores![index]!) ?? 0 + let timeUpdate = TimeUpdate(timestamp: timeStamp, isTotal: false, cornellScore: corScore, opponentScore: oppScore) + corTotal += corScore + 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) + } + }) + } + } + return updates + } + + mutating func parseBoxScore(_ boxScore: [BoxScoreItem]) -> [GameUpdate] { + var updates: [GameUpdate] = [] + // Parse boxScore and map into `GameUpdate` array + + for entry in boxScore { + let team = entry.team + let period = entry.period + let description = entry.description + let time = entry.time ?? "N/A" + let corScore = entry.corScore + let oppScore = entry.oppScore + + let timestamp: Int + if period == "1st" { + timestamp = 1 + } else if period == "2nd" { + timestamp = 2 + } else if let parsed = Int(period.dropLast(2)) { + timestamp = parsed + } else { + // Invalid period + timestamp = -1 + } + + let isCornell = team == "COR" + let eventParty = EventParty(team: team) + + let gameUpdate = GameUpdate(timestamp: timestamp, isTotal: false, cornellScore: corScore, opponentScore: oppScore, time: time, isCornell: isCornell, eventParty: eventParty, description: description) + + updates.append(gameUpdate) + } + + return updates + } + + // update the opponent team + func fetchAndUpdateOpponent(opponentId: String, completion: @escaping (Game) -> Void) { + fetchOpponentTeam(id: opponentId) { team in + DispatchQueue.main.async { + var updatedGame = self + updatedGame.opponent = team ?? Team.defaultTeam() + completion(updatedGame) + } + } + } + + static func parseDate(dateString: String, timeString: String) -> Date { + // parse the date without year + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "MMM d (EEE)" // Matches "Feb 23 (Fri)" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Ensures consistent parsing + dateFormatter.timeZone = TimeZone(abbreviation: "UTC") // Adjust timezone if necessary + let parsedDate = dateFormatter.date(from: dateString) ?? Date() + + // parse the time + let timeFormatter = DateFormatter() + timeFormatter.dateFormat = "h:mm a" // Matches "4:00 p.m." + timeFormatter.locale = Locale(identifier: "en_US_POSIX") + timeFormatter.timeZone = TimeZone(abbreviation: "UTC") + let parsedTime = timeFormatter.date(from: timeString) ?? Date() + + // get the current year + let calendar = Calendar.current + let currentYear = calendar.component(.year, from: Date()) + + // set the year of the parsed date to the current year + // Set the year of the parsed date to the current year + var dateComponents = calendar.dateComponents([.month, .day], from: parsedDate) + dateComponents.year = currentYear + let timeComponents = calendar.dateComponents([.hour, .minute], from: parsedTime) + dateComponents.hour = timeComponents.hour + dateComponents.minute = timeComponents.minute + + // returns the date with the year set to the current year, callback to current date if parsing fails + return calendar.date(from: dateComponents) ?? Date() + } +} + +struct TimeUpdate { + var id: UUID = UUID() + var timestamp: Int + var isTotal: Bool + var cornellScore: Int + var opponentScore: Int +} + +// for decoding a boxScore from backend +struct BoxScoreItem: Decodable { + let team: String + let period: String + let time: String? + let description: String + let scorer: String? + let assist: String? + let scoreBy: String? + let corScore: Int + let oppScore: Int + + init(item: GamesQuery.Data.Game.BoxScore?) { + if let item = item { + self.team = item.team ?? "" + self.period = item.period ?? "" + self.time = item.time ?? "" + self.description = item.description ?? "N/A" + self.scorer = item.scorer ?? "" + self.assist = item.assist ?? "" + self.scoreBy = item.scoreBy ?? "" + self.corScore = item.corScore ?? 0 + self.oppScore = item.oppScore ?? 0 + } else { + self.team = "" + self.period = "" + self.time = "" + self.description = "N/A" + self.scorer = "" + self.assist = "" + self.scoreBy = "" + self.corScore = 0 + self.oppScore = 0 + } + } +} + +func decodeBoxScoreArray(boxScores: [GamesQuery.Data.Game.BoxScore?]?) -> [BoxScoreItem] { + var result: [BoxScoreItem] = [] + if let boxScores = boxScores { + for score in boxScores { + if score != nil { + result.append(BoxScoreItem(item: score)) + } + } + } + return result +} + +func fetchOpponentTeam(id: String, completion: @escaping (Team?) -> Void) { + NetworkManager.shared.fetchTeamById(by: id) { team, error in + if let result = team { + completion(Team(team: result)) + } else if let error = error { +// print("Error in fetchOpponentTeam: \(error.localizedDescription)") + completion(nil) + } + } +} + +struct Team { + var id: String + var color: String + var image: String + var name: String + + init(team: GetTeamByIdQuery.Data.Team) { + self.id = team.id ?? "N/A" + self.color = team.color + self.image = team.image ?? "DEFAULT IMAGE URL" // TODO: make a defualt image url + self.name = team.name + } + + init(id: String, color: String, image: String, name: String) { + self.id = id + self.color = color + self.image = image + self.name = name + } +} + +extension Team { + static func defaultTeam() -> Team { + return Team(id: "N/A", color: "gray", image: "default_image_url", name: "Unknown") + } +} + +struct GameUpdate : Hashable { + var id: UUID = UUID() // discard + var timestamp: Int // period + var isTotal: Bool // discard + var cornellScore: Int // corScore + var opponentScore: Int // oppScore + var time: String // time + var isCornell: Bool // + var eventParty: EventParty // team + var description: String // description +} + +enum EventParty { + case Cornell + case Neither + case Opponent + + init(team: String?) { + switch team { + case "COR": + self = .Cornell + default: + self = .Opponent + } + } } // Enums for various types @@ -81,6 +361,19 @@ enum Sport : String, Identifiable, CaseIterable, CustomStringConvertible { case SprintFootball case Wrestling + // init from a string from backend (might include spaces) + init?(normalizedValue: String) { + // Normalize the input by removing spaces and making it case insensitive + let cleanedValue = normalizedValue.replacingOccurrences(of: " ", with: "").lowercased() + for sport in Sport.allCases { + if sport.rawValue.lowercased() == cleanedValue { + self = sport + return + } + } + return nil + } + // Make a to string function var description: String { switch self { @@ -152,7 +445,17 @@ enum Sex : Identifiable, CaseIterable, CustomStringConvertible { case .Women: return "Women's" } - + } + + var filterDescription: String { + switch self { + case .Both: + return "All" + case .Men: + return "Mens" + case .Women: + return "Womens" + } } // This is strictly for filtering purposes, all datum should have one of Men or Women static func index(of sex: Sex) -> Int? { @@ -163,21 +466,39 @@ enum Sex : Identifiable, CaseIterable, CustomStringConvertible { // TEMP Dummy data extension Game { static let dummyData: [Game] = [ - Game(opponent: .Penn, 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), - Game(opponent: .Harvard, city: "Cambridge", state: "MA", date: Date.dateComponents(year: 2024, month: 5, day: 21, hour: 10, minute: 0), sport: .Football, address: "1 Fake St", sex: .Women), - Game(opponent: .Princeton, 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), - Game(opponent: .Yale, 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), - Game(opponent: .Brown, 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), - Game(opponent: .Dartmouth, 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), - Game(opponent: .Columbia, 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), + 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: 12, day: 6, hour: 14, minute: 0), sport: .Basketball, address: "0 Fake St", sex: .Men, timeUpdates: [], gameUpdates: []), + 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: 12, day: 6, hour: 14, minute: 0), sport: .Basketball, address: "0 Fake St", sex: .Men, timeUpdates: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: true, cornellScore: 10, opponentScore: 7, time: "05/19/2023", isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD"), GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: "05/19/2023", 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: "Cambridge", state: "MA", date: Date.dateComponents(year: 2024, month: 5, day: 21, hour: 10, minute: 0), sport: .Football, address: "1 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: "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: "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: .Penn, 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), - Game(opponent: .Harvard, city: "Cambridge", state: "MA", date: Date.dateComponents(year: 2024, month: 5, day: 21, hour: 10, minute: 0), sport: .Football, address: "1 Fake St", sex: .Men), - Game(opponent: .Princeton, 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), - Game(opponent: .Yale, 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), - Game(opponent: .Brown, 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), - Game(opponent: .Dartmouth, 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), - Game(opponent: .Columbia, 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) + 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")]), + 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: "Cambridge", state: "MA", date: Date.dateComponents(year: 2024, month: 5, day: 21, hour: 10, minute: 0), sport: .Football, address: "1 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: "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: "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] = [ +// Game(opponent: "Penn", 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: [TimeUpdate(timestamp: 1, isTotal: false, cornellScore: 13, opponentScore: 7)], gameUpdates: [GameUpdate(timestamp: 1, isTotal: false, cornellScore: 10, opponentScore: 7, time: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), +// Game(opponent: "Harvard", city: "Cambridge", state: "MA", date: Date.dateComponents(year: 2024, month: 5, day: 21, hour: 10, minute: 0), sport: .Football, address: "1 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: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), +// Game(opponent: "Princeton", 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: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), +// Game(opponent: "Yale", 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: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), +// Game(opponent: "Brown", 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: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), +// Game(opponent: "Dartmouth", 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: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), +// Game(opponent: "Columbia", 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: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), +// +// Game(opponent: "Penn", 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: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), +// Game(opponent: "Harvard", city: "Cambridge", state: "MA", date: Date.dateComponents(year: 2024, month: 5, day: 21, hour: 10, minute: 0), sport: .Football, address: "1 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: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), +// Game(opponent: "Princeton", 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: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), +// Game(opponent: "Yale", 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: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), +// Game(opponent: "Brown", 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: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), +// Game(opponent: "Dartmouth", 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: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]), +// Game(opponent: "Columbia", 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: Date.dateComponents(year: 2024, month: 5, day: 19, hour: 6, minute: 21), isCornell: true, eventParty: EventParty.Cornell, description: "Zhao, Alan field goal attempt from 24 GOOD")]) +// ] } diff --git a/score-ios/Models/GraphQL/Game.graphql b/score-ios/Models/GraphQL/Game.graphql new file mode 100644 index 0000000..d6629f5 --- /dev/null +++ b/score-ios/Models/GraphQL/Game.graphql @@ -0,0 +1,27 @@ +query Games { + games { + id + city + date + gender + location + opponentId + result + sport + state + time + scoreBreakdown + boxScore { + team + period + time + description + scorer + assist + scoreBy + corScore + oppScore + } + } +} + diff --git a/score-ios/Models/GraphQL/Team.graphql b/score-ios/Models/GraphQL/Team.graphql new file mode 100644 index 0000000..5dba75c --- /dev/null +++ b/score-ios/Models/GraphQL/Team.graphql @@ -0,0 +1,8 @@ +query GetTeamById($id: String!) { + team(id: $id) { + id + color + image + name + } +} diff --git a/score-ios/Models/GraphQL/schema.graphqls b/score-ios/Models/GraphQL/schema.graphqls new file mode 100644 index 0000000..d077aec --- /dev/null +++ b/score-ios/Models/GraphQL/schema.graphqls @@ -0,0 +1,111 @@ +""" +A directive used by the Apollo iOS client to annotate operations or fragments that should be used exclusively for generating local cache mutations instead of as standard operations. +""" +directive @apollo_client_ios_localCacheMutation on QUERY | MUTATION | SUBSCRIPTION | FRAGMENT_DEFINITION + +""" +A directive used by the Apollo iOS code generation engine to generate custom import statements in operation or fragment definition files. An import statement to import a module with the name provided in the `module` argument will be added to the generated definition file. +""" +directive @import( + """The name of the module to import.""" + module: String! +) repeatable on QUERY | MUTATION | SUBSCRIPTION | FRAGMENT_DEFINITION + +""" +Directs the executor to defer this fragment when the `if` argument is true or undefined. +""" +directive @defer( + """Deferred when true or undefined.""" + if: Boolean + + """Unique name""" + label: String +) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +type Query { + games: [GameType] + game(id: String!): GameType + gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!): GameType + teams: [TeamType] + team(id: String!): TeamType + teamByName(name: String!): TeamType +} + +""" +A GraphQL type representing a boxscore. + +Attributes: + +""" +type BoxScore { + team: String + period: String + time: String + description: String + scorer: String + assist: String + scoreBy: String + corScore: Int + oppScore: Int +} + +""" +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. + - `opponent_id`: The id of the opposing team. + - `sport`: The sport of the game. + - `state`: The state of the game. + - `time`: The time of the game. +""" +type GameType { + id: String + city: String! + date: String! + gender: String! + location: String + opponentId: String! + result: String + sport: String! + state: String! + time: String + scoreBreakdown: [[String]] + boxScore: [BoxScore] +} + +""" +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. +""" +type TeamType { + id: String + color: String! + image: String + name: String! +} + +type Mutation { + """Creates a new game.""" + createGame(city: String!, date: String!, gender: String!, location: String, opponentId: String!, result: String, sport: String!, state: String!, time: String!): CreateGame + + """Creates a new team.""" + createTeam(color: String!, image: String, name: String!): CreateTeam +} + +type CreateGame { + game: GameType +} + +type CreateTeam { + team: TeamType +} diff --git a/score-ios/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/score-ios/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000..c4cfeca Binary files /dev/null and b/score-ios/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ diff --git a/score-ios/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/score-ios/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 13613e3..cefcc87 100644 --- a/score-ios/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/score-ios/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "AppIcon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/score-ios/Resources/Assets.xcassets/CrossCountry-g.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/CrossCountry-g.imageset/Contents.json index 92f7aea..aa749cb 100644 --- a/score-ios/Resources/Assets.xcassets/CrossCountry-g.imageset/Contents.json +++ b/score-ios/Resources/Assets.xcassets/CrossCountry-g.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "steps.svg", + "filename" : "CorssCountry-g.svg", "idiom" : "universal", "scale" : "1x" }, @@ -17,8 +17,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" } } diff --git a/score-ios/Resources/Assets.xcassets/CrossCountry-g.imageset/CorssCountry-g.svg b/score-ios/Resources/Assets.xcassets/CrossCountry-g.imageset/CorssCountry-g.svg new file mode 100644 index 0000000..f9b1427 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/CrossCountry-g.imageset/CorssCountry-g.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/score-ios/Resources/Assets.xcassets/Soccer-r.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/Soccer-r.imageset/Contents.json new file mode 100644 index 0000000..7bd8a5a --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/Soccer-r.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "sports_and_outdoors.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/score-ios/Resources/Assets.xcassets/Soccer-r.imageset/sports_and_outdoors.png b/score-ios/Resources/Assets.xcassets/Soccer-r.imageset/sports_and_outdoors.png new file mode 100644 index 0000000..5e466ec Binary files /dev/null and b/score-ios/Resources/Assets.xcassets/Soccer-r.imageset/sports_and_outdoors.png differ diff --git a/score-ios/Resources/Assets.xcassets/arrow_back_ios.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/arrow_back_ios.imageset/Contents.json new file mode 100644 index 0000000..c17518f --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/arrow_back_ios.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "arrow_back_ios.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/score-ios/Resources/Assets.xcassets/arrow_back_ios.imageset/arrow_back_ios.svg b/score-ios/Resources/Assets.xcassets/arrow_back_ios.imageset/arrow_back_ios.svg new file mode 100644 index 0000000..da64884 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/arrow_back_ios.imageset/arrow_back_ios.svg @@ -0,0 +1,3 @@ + + + diff --git a/score-ios/Resources/Assets.xcassets/pastGame_arrow_back.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/pastGame_arrow_back.imageset/Contents.json new file mode 100644 index 0000000..7f94316 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/pastGame_arrow_back.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "pastGame_arrow_back.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/score-ios/Resources/Assets.xcassets/pastGame_arrow_back.imageset/pastGame_arrow_back.svg b/score-ios/Resources/Assets.xcassets/pastGame_arrow_back.imageset/pastGame_arrow_back.svg new file mode 100644 index 0000000..45c32e8 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/pastGame_arrow_back.imageset/pastGame_arrow_back.svg @@ -0,0 +1,3 @@ + + + diff --git a/score-ios/Resources/Assets.xcassets/schedule-selected.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/schedule-selected.imageset/Contents.json new file mode 100644 index 0000000..b4d2d90 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/schedule-selected.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "schedule-selected.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/score-ios/Resources/Assets.xcassets/schedule-selected.imageset/schedule-selected.svg b/score-ios/Resources/Assets.xcassets/schedule-selected.imageset/schedule-selected.svg new file mode 100644 index 0000000..ec6f766 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/schedule-selected.imageset/schedule-selected.svg @@ -0,0 +1,3 @@ + + + diff --git a/score-ios/Resources/Assets.xcassets/schedule.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/schedule.imageset/Contents.json new file mode 100644 index 0000000..4e62db3 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/schedule.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "schedule.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/score-ios/Resources/Assets.xcassets/schedule.imageset/schedule.svg b/score-ios/Resources/Assets.xcassets/schedule.imageset/schedule.svg new file mode 100644 index 0000000..ad1b994 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/schedule.imageset/schedule.svg @@ -0,0 +1,3 @@ + + + diff --git a/score-ios/Resources/Assets.xcassets/scoreboard-selected.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/scoreboard-selected.imageset/Contents.json new file mode 100644 index 0000000..2e57606 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/scoreboard-selected.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "scoreboard-selected.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/score-ios/Resources/Assets.xcassets/scoreboard-selected.imageset/scoreboard-selected.svg b/score-ios/Resources/Assets.xcassets/scoreboard-selected.imageset/scoreboard-selected.svg new file mode 100644 index 0000000..ac3477c --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/scoreboard-selected.imageset/scoreboard-selected.svg @@ -0,0 +1,3 @@ + + + diff --git a/score-ios/Resources/Assets.xcassets/scoreboard.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/scoreboard.imageset/Contents.json new file mode 100644 index 0000000..40de7c1 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/scoreboard.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "scoreboard.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/score-ios/Resources/Assets.xcassets/scoreboard.imageset/scoreboard.svg b/score-ios/Resources/Assets.xcassets/scoreboard.imageset/scoreboard.svg new file mode 100644 index 0000000..63b0487 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/scoreboard.imageset/scoreboard.svg @@ -0,0 +1,3 @@ + + + diff --git a/score-ios/Utils/Constants.swift b/score-ios/Utils/Constants.swift index d20795f..094a43d 100644 --- a/score-ios/Utils/Constants.swift +++ b/score-ios/Utils/Constants.swift @@ -45,14 +45,22 @@ struct Constants { enum Fonts { // Make it poppins - static let h1 = Font.custom("Poppins", size: 24) - static let gameScore = Font.custom("Poppins Semibold", size: 18) + static let h1 = Font.custom("Poppins Medium", size: 24) + static let h2 = Font.custom("Poppins SemiBold", size: 18) + static let countdownNum = Font.custom("Poppins Medium", size: 36) + static let gameScore = Font.custom("Poppins SemiBold", size: 18) static let gameTitle = Font.custom("Poppins Medium", size: 18) static let gameText = Font.custom("Poppins Regular", size: 14) static let buttonLabel = Font.custom("Poppins Medium", size: 14) static let gameDate = Font.custom("Poppins Regular", size: 12) static let sportLabel = Font.custom("Poppins Regular", size: 12) static let filterLabel = Font.custom("Poppins Regular", size: 24) + static let semibold24 = Font.custom("Poppins SemiBold", size: 24) + static let semibold18 = Font.custom("Poppins SemiBold", size: 18) + static let medium14 = Font.custom("Poppins Medium", size: 14) + static let medium18 = Font.custom("Poppins Medium", size: 18) + static let regular14 = Font.custom("Poppins Regular", size: 14) + static let bold40 = Font.custom("Poppins Bold", size: 40) static let title = Font.system(size: 36, weight: .bold, design: .default) static let header = Font.system(size: 24, weight: .bold, design: .default) static let subheader = Font.system(size: 18, weight: .bold, design: .default) diff --git a/score-ios/Utils/OrdinalSuffix.swift b/score-ios/Utils/OrdinalSuffix.swift new file mode 100644 index 0000000..bfc7f1f --- /dev/null +++ b/score-ios/Utils/OrdinalSuffix.swift @@ -0,0 +1,28 @@ +// +// OrdinalSuffix.swift +// score-ios +// +// Created by Daniel Chuang on 12/4/24. +// + +import Foundation + +func ordinalSuffix(for number: Int) -> String { + let lastDigit = number % 10 + let lastTwoDigits = number % 100 + + if lastTwoDigits >= 11 && lastTwoDigits <= 13 { + return "th" + } + + switch lastDigit { + case 1: return "st" + case 2: return "nd" + case 3: return "rd" + default: return "th" + } +} + +func ordinalNumberString(for number: Int) -> String { + return "\(number)\(ordinalSuffix(for: number))" +} diff --git a/score-ios/ViewModels/GamesViewModel.swift b/score-ios/ViewModels/GamesViewModel.swift new file mode 100644 index 0000000..8deba34 --- /dev/null +++ b/score-ios/ViewModels/GamesViewModel.swift @@ -0,0 +1,130 @@ +// +// GamesViewModel.swift +// score-ios +// +// Created by Hsia Lu wu on 12/3/24. +// + +import Foundation +import SwiftUI + +class GamesViewModel: ObservableObject { +// @Published var allGames: [Game] = [] +// @Published var upcomingGames: [Game] = [] +// @Published var pastGames: [Game] = [] + + init() { +// fetchAllGames { [weak self] game in +// DispatchQueue.main.async { +// self?.allGames = games +// } +// } + } + + private func getUpcomingGames(allGames: [Game]) -> [Game] { + var upcomingGames: [Game] = [] + + allGames.forEach { game in + let now = Date() + let twoHours: TimeInterval = 2 * 60 * 60 + 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 + + if isLive { + upcomingGames.insert(game, at: 0) + } else if isUpcoming { + upcomingGames.append(game) + } else if isFinishedToday { + upcomingGames.append(game) + } + } + + return upcomingGames + } + + private func getPastGames(allGames: [Game]) -> [Game] { + var pastGames: [Game] = [] + + allGames.forEach { game in + let now = Date() + let calendar = Calendar.current + let startOfToday = calendar.startOfDay(for: now) + + let isFinishedByToday = game.date < startOfToday + + if isFinishedByToday { + if(!game.gameUpdates.isEmpty) { + pastGames.append(game) + } + } + } + return pastGames + } +} + +//func fetchAllGames() -> [Game] { +// var games: [Game] = [] +// NetworkManager.shared.fetchGames { fetchedGames, error in +// if let fetchedGames = fetchedGames { +// var updatedGames: [Game] = [] +// let dispatchGroup = DispatchGroup() +// +// fetchedGames.forEach { gameData in +// let game = Game(game: gameData) +// dispatchGroup.enter() // enter the dispatchGroup +// +// game.fetchAndUpdateOpponent(opponentId: gameData.opponentId) { updatedGame in +// updatedGames.append(updatedGame) +// print("UpdatedGame: \(updatedGame.sport) game with \(updatedGame.opponent.name)") +// dispatchGroup.leave() +// } +// } +// +// dispatchGroup.notify(queue: .main) { +// games = updatedGames +// } +// print(games.isEmpty) +// games.forEach { game in +// print("Game: \(game.sport) game with \(game.opponent.name)") +// } +// } +// else if let error = error { +// print("Error in fetchAllGames: \(error.localizedDescription)") +// } +// } +// return games +//} + +//func fetchAllGames(completion: @escaping ([Game]) -> Void) -> [Game] { +// var games: [Game] = [] +// NetworkManager.shared.fetchGames { fetchedGames, error in +// if let fetchedGames = fetchedGames { +// var updatedGames: [Game] = [] +// let dispatchGroup = DispatchGroup() +// +// fetchedGames.forEach { gameData in +// let game = Game(game: gameData) +// dispatchGroup.enter() // enter the dispatchGroup +// +// game.fetchAndUpdateOpponent(opponentId: gameData.opponentId) { updatedGame in +// updatedGames.append(updatedGame) +// print("UpdatedGame: \(updatedGame.sport) game with \(updatedGame.opponent.name)") +// dispatchGroup.leave() +// } +// } +// +// dispatchGroup.notify(queue: .main) { +// games = updatedGames +// completion(updatedGames) +// } +// } +// else if let error = error { +// print("Error in fetchAllGames: \(error.localizedDescription)") +// completion([]) +// } +// } +//} diff --git a/score-ios/Views/FilterTile.swift b/score-ios/Views/FilterTile.swift index 505afd5..e9e0d5e 100644 --- a/score-ios/Views/FilterTile.swift +++ b/score-ios/Views/FilterTile.swift @@ -18,9 +18,10 @@ struct FilterTile : View { VStack(spacing: 6) { Image(imageName) .resizable() - .renderingMode(.template) + .renderingMode(.original) .frame(width: 32, height: 32) - .foregroundStyle(selected ? Constants.Colors.selected : Constants.Colors.iconGray) +// .foregroundStyle(selected ? Constants.Colors.selected : Constants.Colors.iconGray) + Text(sport.description) .foregroundStyle(selected ? Constants.Colors.selected : Constants.Colors.iconGray) .font(Constants.Fonts.sportLabel) diff --git a/score-ios/Views/GameDetailedScoreView.swift b/score-ios/Views/GameDetailedScoreView.swift index c3496cf..c642ab8 100644 --- a/score-ios/Views/GameDetailedScoreView.swift +++ b/score-ios/Views/GameDetailedScoreView.swift @@ -17,25 +17,30 @@ struct GameDetailedScoreView: View { // Banner banner - VStack { - // information - information - .frame(maxWidth: .infinity, alignment: .leading) - // score box - scoreBox - .frame(maxWidth: .infinity, alignment: .leading) - } - .padding(.leading, 24) - .padding(.trailing, 24) - - // score summary tab - summaryTab + // information + information + .frame(maxWidth: .infinity, alignment: .leading).padding(.leading, 24) + .padding(.trailing, 24) - // score summary if (gameStarted) { - gameSummary + VStack { + scoreBox + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.leading, 24) + .padding(.trailing, 24) + // score summary tab + summaryTab + + // score summary + if (!game.timeUpdates.isEmpty) { + gameSummary + } else { + noGameSummary + } } else { - noGameSummary + countDown + .padding(.top, 40) } } .navigationBarTitleDisplayMode(.inline) @@ -73,7 +78,8 @@ extension GameDetailedScoreView { Spacer() Text("0 - 0") .font(Constants.Fonts.title) - .foregroundColor(.gray) + .foregroundColor(.white) + .shadow(radius: 4) Spacer() Image("penn_logo") @@ -92,28 +98,76 @@ extension GameDetailedScoreView { private var information: some View { VStack(alignment: .leading, spacing: 4) { Text("Men's Football") - .font(Constants.Fonts.subheader) - Text("Cornell vs. " + game.opponent.rawValue) - .font(Constants.Fonts.header) + .font(Constants.Fonts.medium14) + Text("Cornell vs. " + game.opponent.name) + .font(Constants.Fonts.semibold24) HStack() { Image("Location-g") .resizable() - .frame(width: 23, height: 26) + .frame(width: 13, height: 19) Text("Ithaca (Schoellkopf)") Image("Alarm") .resizable() - .frame(width: 24, height: 24) + .frame(width: 19.78, height: 18.34) Text(Date.dateToString(date: game.date)) - } .font(Constants.Fonts.gameText) .foregroundColor(.gray) .padding(.top, 10) } -// .padding(.leading, 24) -// .padding(.trailing, 24) - + } + + // countdown + private var countDown: some View { + VStack { + Image("Hourglass") + .resizable() + .frame(width: 93, height: 118) + + Text("Time Until Start") + .font(Constants.Fonts.h2) + .padding(.top, 24) + + HStack { + Text("2") + .font(Constants.Fonts.countdownNum) + Text("days") + .font(Constants.Fonts.gameText) + Text("0") + .font(Constants.Fonts.countdownNum) + Text("hours") + .font(Constants.Fonts.gameText) + } + .padding(.top, 8) + + // Add to Calendar Button + Button(action: { + // TODO: action + }) { + HStack { + Image("Calendar") + .resizable() + .frame(width: 24, height: 24) + Text("Add to Calendar") + .font(Constants.Fonts.buttonLabel) + } + .foregroundColor(.white) + .padding(.horizontal, 16) + .padding(.vertical, 10) + .frame(height: 48) + .background( + Constants.Colors.primary_red + ) + .overlay( + RoundedRectangle(cornerRadius: 30) + .stroke(Color.black.opacity(0.1), lineWidth: 1) + .shadow(color: Color.black.opacity(0.25), radius: 5, x: 0, y: 2) + ) + .clipShape(RoundedRectangle(cornerRadius: 30)) // Clip to shape to ensure rounded corners + } + .padding(.bottom, 36) + } } private var firstRow: some View { @@ -185,7 +239,7 @@ extension GameDetailedScoreView { private var thirdRow: some View { HStack { - Text(game.opponent.rawValue) + Text(game.opponent.name) .font(Constants.Fonts.gameText) .foregroundStyle(.gray) .frame(width: 60, alignment: .leading) @@ -220,21 +274,22 @@ extension GameDetailedScoreView { } private var scoreBox: some View { - VStack { + VStack(spacing: 0) { firstRow secondRow + Rectangle() // Custom red divider + .fill(Constants.Colors.primary_red) + .frame(height: 0.8) thirdRow } .frame(maxWidth: .infinity) .background(Constants.Colors.white) - .clipShape(RoundedRectangle(cornerRadius: 19)) + .clipShape(RoundedRectangle(cornerRadius: 8)) .background( - RoundedRectangle(cornerRadius: 12) - .stroke(Constants.Colors.gray_border, lineWidth: 1) + RoundedRectangle(cornerRadius: 8) + .stroke(Constants.Colors.primary_red, lineWidth: 1) .shadow(radius: 5) ) -// .padding(.leading, 24) -// .padding(.trailing, 24) } private var summaryTab: some View { @@ -243,12 +298,12 @@ extension GameDetailedScoreView { } label: { HStack { Text("Score Summary") - .font(Constants.Fonts.navBarTitle) + .font(Constants.Fonts.medium18) .foregroundStyle(.gray) Spacer() Image("Right-arrow") .resizable() - .frame(width: 13, height: 10) + .frame(width: 9.87, height: 18.57) } .padding(.leading, 15) .padding(.trailing, 17) @@ -268,10 +323,13 @@ extension GameDetailedScoreView { Image("speaker") .resizable() .frame(width: 90, height: 90) - Text("Scores will be up soon.") - .font(Constants.Fonts.caption) + Text("No Scores Yet.") + .font(Constants.Fonts.medium18) + Text("Check back here later!") + .font(Constants.Fonts.regular14) .foregroundStyle(Constants.Colors.gray_text) } .padding(.top, 40) + .padding(.bottom, 50) } } diff --git a/score-ios/Views/GameTile.swift b/score-ios/Views/GameTile.swift index 1afcedc..8831c2f 100644 --- a/score-ios/Views/GameTile.swift +++ b/score-ios/Views/GameTile.swift @@ -18,11 +18,19 @@ struct GameTile: View { // Opponent Logo, Opponent Name | Sport Icon, Sex Icon HStack(spacing: 8) { HStack(spacing: 8) { - Image(game.opponent.rawValue) +// Image(game.opponent) + AsyncImage(url: URL(string: game.opponent.image)) {image in + image.resizable() + } placeholder: { + Constants.Colors.gray_icons + } + .frame(width: 20, height: 20) - Text(game.opponent.rawValue) + Text(game.opponent.name) .font(Constants.Fonts.gameTitle) - } .padding(.leading, 20) + .lineLimit(1) + } + .padding(.leading, 20) Spacer() diff --git a/score-ios/Views/GameView.swift b/score-ios/Views/GameView.swift index a460d75..ccbc946 100644 --- a/score-ios/Views/GameView.swift +++ b/score-ios/Views/GameView.swift @@ -9,128 +9,503 @@ import SwiftUI struct GameView : View { var game : Game + @State var viewState: Int = 0 + @State var dayFromNow: Int = 0 + @State var hourFromNow: Int = 0 + @State var corScore1: String = "-" + @State var corScore2: String = "-" + @State var corScore3: String = "-" + @State var corScore4: String = "-" + @State var corScoreTotal: String = "0" + @State var oppScore1: String = "-" + @State var oppScore2: String = "-" + @State var oppScore3: String = "-" + @State var oppScore4: String = "-" + @State var oppScoreTotal: String = "0" + @Environment(\.presentationMode) var presentationMode + // 0: hasn't started + // 1: game started (no updates yet) + // 2: game in progress / game finished + var body : some View { NavigationView { - VStack { - // Banner - HStack { - Spacer() - Image("Cornell") - .resizable() - .frame(width: 72, height: 72) - Spacer() - Text("0 - 0") - .font(Constants.Fonts.title) - .foregroundColor(.gray) - Spacer() - - // TODO: Fix dimension of logos - Image("penn_logo") - .resizable() - .frame(width: 72, height: 72) - Spacer() + ZStack { + switch viewState { + case 0: hasntStartedView + case 1: gameStartedView + case 2: gameInProgressView + .onAppear { updateScores() } + default: hasntStartedView } - .padding() - .frame(height: 185) - .background(LinearGradient(gradient: Gradient(colors: [ - Color(red: 179 / 255, green: 27 / 255, blue: 27 / 255, opacity: 0.4), - Color(red: 1 / 255, green: 31 / 255, blue: 91 / 255, opacity: 0.4) - ]), startPoint: .leading, endPoint: .trailing)) - - // Game information - VStack(alignment: .leading, spacing: 4) { - Text("Men's Football") - .font(Constants.Fonts.subheader) - Text("Cornell vs. " + game.opponent.rawValue) - .font(Constants.Fonts.header) - - HStack(spacing: 10) { - HStack { - Image("Alarm") - Text(Date.dateToString(date: game.date)) - } - - // TODO: Fix location - HStack { - Image("Location-g") - Text("Ithaca (Schoellkopf)") - } + } + .onAppear { computeTimeFromNow() } + .onAppear { updateViewState() } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text("Game Details") + .font(.system(size: 27, weight: .regular)) + } + ToolbarItem(placement: .navigationBarLeading) { + Button { + presentationMode.wrappedValue.dismiss() + } label: { + Image("arrow_back_ios") + .resizable() + .frame(width: 9.87, height: 18.57) } - .font(Constants.Fonts.gameDate) - .foregroundColor(.gray) - .padding(.top, 10) } - .padding() - .padding(.trailing, 100) + } + .navigationBarBackButtonHidden() + .toolbarBackground(Color.white, for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + } + } +} + +// MARK: Functions +extension GameView { + private func computeTimeFromNow() { + let now = Date() + let calendar = Calendar.current + let components = calendar.dateComponents([.day, .hour], from: now, to: game.date) + // Extract the values safely + self.dayFromNow = components.day ?? 0 + self.hourFromNow = components.hour ?? 0 + } + + private func updateViewState() { + let now = Date() + if (game.timeUpdates.isEmpty) { + if (game.date <= now) { + // game started but no score updates available + self.viewState = 1 + } else { + // game hasn't started + self.viewState = 0 + } + } else { + // game score updates available + self.viewState = 2 + } + } + + private func updateScores() { + if (game.timeUpdates.count >= 2) { + // scores for more than 4 rounds are available, display the first four rounds + let timeUpdate1 = game.timeUpdates[0] + corScore1 = String(timeUpdate1.cornellScore) + oppScore1 = String(timeUpdate1.opponentScore) + } + + if (game.timeUpdates.count >= 3) { + // scores for more than 4 rounds are available, display the first four rounds + let timeUpdate2 = game.timeUpdates[1] + corScore2 = String(timeUpdate2.cornellScore) + oppScore2 = String(timeUpdate2.opponentScore) + } else { + corScore3 = "-" + corScore4 = "-" + corScoreTotal = "-" + oppScore3 = "-" + oppScore4 = "-" + oppScoreTotal = "-" + } + + + if (game.timeUpdates.count >= 4) { + // scores for more than 4 rounds are available, display the first four rounds + let timeUpdate3 = game.timeUpdates[2] + corScore3 = String(timeUpdate3.cornellScore) + oppScore3 = String(timeUpdate3.opponentScore) + } else { + corScore4 = "-" + corScoreTotal = "-" + oppScore4 = "-" + oppScoreTotal = "-" + } + + if (game.timeUpdates.count >= 5) { + // scores for more than 4 rounds are available, display the first four rounds + let timeUpdate4 = game.timeUpdates[3] + corScore4 = String(timeUpdate4.cornellScore) + oppScore4 = String(timeUpdate4.opponentScore) + if (game.timeUpdates[game.timeUpdates.count - 1].isTotal) { + let totalScores = game.timeUpdates[game.timeUpdates.count - 1] + corScoreTotal = String(totalScores.cornellScore) + oppScoreTotal = String(totalScores.opponentScore) + } else { + corScoreTotal = "-" + oppScoreTotal = "-" + } + } + + // get total scores + if (!game.gameUpdates.isEmpty) { + corScoreTotal = String(game.gameUpdates[game.gameUpdates.count-1].cornellScore) + oppScoreTotal = String(game.gameUpdates[game.gameUpdates.count-1].opponentScore) + } + } +} + +// MARK: Components +extension GameView { + private var banner: some View { + HStack { + Spacer() + Image("Cornell") + .resizable() + .frame(width: 72, height: 72) + Spacer() + Text("\(corScoreTotal) - \(oppScoreTotal)") + .font(Constants.Fonts.bold40) + .foregroundColor(Constants.Colors.white) + Spacer() + AsyncImage(url: URL(string: game.opponent.image)) { + image in + image.image?.resizable() + } + .frame(width: 72, height: 72) + + Spacer() + } + .padding() + .frame(height: 185) + .background(LinearGradient(gradient: Gradient(colors: [ + Color(red: 179 / 255, green: 27 / 255, blue: 27 / 255, opacity: 0.4), + Color(red: 1 / 255, green: 31 / 255, blue: 91 / 255, opacity: 0.4) + ]), startPoint: .leading, endPoint: .trailing)) + } + + private var gameInfo: some View { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("\(game.sex.description) \(game.sport.description)") + .font(Constants.Fonts.subheader) + Text("Cornell vs. " + game.opponent.name) + .font(Constants.Fonts.header) - // Countdown - VStack { - Image("Hourglass") - - Text("Time Until Start") - .font(Constants.Fonts.bodyBold) - + HStack(spacing: 10) { HStack { - Text("2") - .font(Constants.Fonts.header) - - Text("days") - .font(Constants.Fonts.gameText) - Text("0") - .font(Constants.Fonts.header) - - Text("hours") - .font(Constants.Fonts.gameText) + Image("Location-g") + Text(game.address) } - .padding(.top, 5) - // Add to Calendar Button - Button(action: { - // TODO: action - }) { - HStack { - Image("Calendar") - .resizable() - .frame(width: 24, height: 24) - Text("Add to Calendar") - .font(Constants.Fonts.buttonLabel) - } - .foregroundColor(.white) - .padding(.horizontal, 16) - .padding(.vertical, 10) - .background( - Constants.Colors.primary_red - ) - .overlay( - RoundedRectangle(cornerRadius: 30) - .stroke(Color.black.opacity(0.1), lineWidth: 1) - .shadow(color: Color.black.opacity(0.25), radius: 5, x: 0, y: 2) - ) - .clipShape(RoundedRectangle(cornerRadius: 30)) // Clip to shape to ensure rounded corners + HStack { + Image("Alarm") + Text(Date.dateToString(date: game.date)) } - .padding(.top, 68) } - .padding(.top, 50) + .font(Constants.Fonts.gameDate) + .foregroundColor(.gray) + .padding(.top, 10) } - .padding(.bottom, 25) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .principal) { - Text("Game Details") - .font(.system(size: 27, weight: .regular)) + Spacer() + } + } + + private var countdown: some View { + VStack { + VStack { + Image("Hourglass") + .resizable() + .frame(width: 93, height: 118) + Text("Time Until Start") + .font(Constants.Fonts.h2) + .padding(.top, 24) + + HStack { + Text(String(dayFromNow)) + .font(Constants.Fonts.countdownNum) + Text("days") + .font(Constants.Fonts.gameText) + Text(String(hourFromNow)) + .font(Constants.Fonts.countdownNum) + Text("hours") + .font(Constants.Fonts.gameText) } - ToolbarItem(placement: .navigationBarLeading) { - Button(action: { - // TODO: Add action for back button - }) { - Image(systemName: "chevron.left") - .foregroundColor(.primary) - } + .padding(.top, 8) + } + .padding(.top, 20) + + + // Calendar Button + Button(action: { + // TODO: action + }) { + HStack { + Image("Calendar") + .resizable() + .frame(width: 24, height: 24) + Text("Add to Calendar") + .font(Constants.Fonts.buttonLabel) } + .foregroundColor(.white) + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background( + Constants.Colors.primary_red + ) + .overlay( + RoundedRectangle(cornerRadius: 30) + .stroke(Color.black.opacity(0.1), lineWidth: 1) + .shadow(color: Color.black.opacity(0.25), radius: 5, x: 0, y: 2) + ) + .clipShape(RoundedRectangle(cornerRadius: 30)) // Clip to shape to ensure rounded corners + } + .padding(.top, 68) + } + } + + private var firstRow: some View { + HStack { + HStack{ + Text("1") + .font(Constants.Fonts.gameText) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + Text("2") + .font(Constants.Fonts.gameText) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + Text("3") + .font(Constants.Fonts.gameText) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + Text("4") + .font(Constants.Fonts.gameText) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + Text("Total") + .font(Constants.Fonts.gameText) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) } + .padding(.leading, 60) + .padding(.trailing, 10) } + .frame(height: 40) + .background(Constants.Colors.primary_red) } + + private var secondRow: some View { + HStack { + Text("Cornell") + .font(Constants.Fonts.gameText) + .foregroundStyle(.gray) + .frame(width: 60, alignment: .leading) + Text(corScore1) + .font(Constants.Fonts.gameText) + .foregroundStyle(.gray) + .frame(width: 24) + Text(corScore2) + .font(Constants.Fonts.gameText) + .foregroundStyle(.gray) + .frame(width: 24) + .padding(.leading, 29.5) + Text(corScore3) + .font(Constants.Fonts.gameText) + .foregroundStyle(.gray) + .frame(width: 24) + .padding(.leading, 29.5) + Text(corScore4) + .font(Constants.Fonts.gameText) + .foregroundStyle(.gray) + .frame(width: 24) + .padding(.leading, 29.5) + Text(corScoreTotal) + .font(Constants.Fonts.gameText) + .foregroundStyle(.gray) + .frame(width: 24) + .padding(.leading, 29.5) + .padding(.trailing, 12) + } + .frame(height: 40) + .padding(.leading, 10) + } + + private var thirdRow: some View { + HStack { + Text(game.opponent.name) + .lineLimit(1) + .font(Constants.Fonts.gameText) + .foregroundStyle(.gray) + .frame(width: 60, alignment: .leading) + Text(oppScore1) + .font(Constants.Fonts.gameText) + .foregroundStyle(.gray) + .frame(width: 24) + Text(oppScore2) + .font(Constants.Fonts.gameText) + .foregroundStyle(.gray) + .frame(width: 24) + .padding(.leading, 29.5) + Text(oppScore3) + .font(Constants.Fonts.gameText) + .foregroundStyle(.gray) + .frame(width: 24) + .padding(.leading, 29.5) + Text(oppScore4) + .font(Constants.Fonts.gameText) + .foregroundStyle(.gray) + .frame(width: 24) + .padding(.leading, 29.5) + Text(oppScoreTotal) + .font(Constants.Fonts.gameText) + .foregroundStyle(.gray) + .frame(width: 24) + .padding(.leading, 29.5) + .padding(.trailing, 12) + } + .frame(height: 40) + .padding(.leading, 10) + } + + private var scoreBox: some View { + VStack(spacing: 0) { + firstRow + secondRow + Rectangle() // Custom red divider + .fill(Constants.Colors.primary_red) + .frame(height: 0.8) + thirdRow + } + .frame(maxWidth: .infinity) + .background(Constants.Colors.white) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .background( + RoundedRectangle(cornerRadius: 8) + .stroke(Constants.Colors.primary_red, lineWidth: 1) + .shadow(radius: 5) + ) + } + + private var summaryTab: some View { + NavigationLink(destination: ScoringSummary(game: game)) { + HStack { + Text("Score Summary") + .font(Constants.Fonts.medium18) + .foregroundStyle(.gray) + Spacer() + Image("Right-arrow") + .resizable() + .frame(width: 9.87, height: 18.57) + } + .padding(.leading, 15) + .padding(.trailing, 17) + } + } + + private var gameSummary: some View { + VStack { + ForEach(Array(game.gameUpdates.prefix(3)).indices, id: \.self) { i in + if game.gameUpdates[i].isCornell { + ScoringUpdateCell(update: game.gameUpdates[i], img: "Cornell") + } else { + ScoringUpdateCell(update: game.gameUpdates[i], img: game.opponent.image) + } + + // Add a divider except after the last cell + if i < game.gameUpdates.prefix(3).count - 1 { + Divider() + } + } + } + } + + private var noGameSummary: some View { + VStack { + Image("speaker") + .resizable() + .frame(width: 90, height: 90) + Text("No Scores Yet.") + .font(Constants.Fonts.medium18) + Text("Check back here later!") + .font(Constants.Fonts.regular14) + .foregroundStyle(Constants.Colors.gray_text) + } + .frame(maxWidth: .infinity) + } + + private var hasntStartedView: some View { + VStack { + // Banner + banner + Spacer() + // Game information + gameInfo + .padding(.leading, 24) + .padding(.trailing, 24) + .padding(.top, 24) + + // Countdown + countdown + .padding(.bottom, 36) + } + } + + private var gameStartedView: some View { + VStack { + banner + gameInfo + .padding(.leading, 24) + .padding(.top, 24) + + VStack { + scoreBox + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.leading, 24) + .padding(.trailing, 24) + .padding(.top, 24) + + // score summary tab + summaryTab + .padding(.top, 24) + + gameSummary + .overlay { + if (game.gameUpdates.count < 3) { + noGameSummary + .padding(.top, 150) + .frame(maxWidth: .infinity) + } + } + .frame(maxWidth: .infinity) + + Spacer() + } + } + + private var gameInProgressView: some View { + VStack { + banner + Spacer() + gameInfo + .padding(.leading, 24) + .padding(.top, 24) + + VStack { + scoreBox + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.leading, 24) + .padding(.trailing, 24) + .padding(.top, 24) + + // score summary tab + summaryTab + .padding(.top, 24) + gameSummary + } + } + + +} + +#Preview { + GameView(game: Game.dummyData[0]) } #Preview { diff --git a/score-ios/Views/HomeView.swift b/score-ios/Views/HomeView.swift new file mode 100644 index 0000000..04041ad --- /dev/null +++ b/score-ios/Views/HomeView.swift @@ -0,0 +1,262 @@ +// +// HomeView.swift +// score-ios +// +// Created by Hsia Lu wu on 11/6/24. +// + +import SwiftUI + +struct HomeView: View { + // State variables + @State private var selectedSex : Sex = .Both + @State private var selectedSport : Sport = .All + var paddingMain : CGFloat = 20 + @State private var selectedCardIndex: Int = 0 + @State private var games: [Game] = [] + @State private var allGames: [Game] = [] + @State private var upcomingGames: [Game] = [] + @State private var errorMessage: String? + + // Main view + var body: some View { + NavigationView { + VStack(spacing: 0) { + carousel + + VStack { + Text("Game Schedule") + .font(Constants.Fonts.semibold24) + .frame(maxWidth: .infinity, alignment: .leading) // Align to the left + + genderSelector + .frame(maxWidth: .infinity, alignment: .center) + sportSelector + } + .padding(.bottom, 16) + + // Seperator line + Divider() + .background(.clear) + + // List of games + gameList + .overlay { + if games.isEmpty { + NoGameView() + } + } + } + .safeAreaInset(edge: .bottom, content: { + Color.clear.frame(height: 20) + }) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.leading, paddingMain) + .padding(.trailing, paddingMain) + + } + .onAppear { + fetchGames() + } + .onChange(of: selectedSport) { + filterUpcomingGames() + } + .onChange(of: selectedSex) { + filterUpcomingGames() + } + } +} + +#Preview { + HomeView() +} + +// MARK: Functions +extension HomeView { + private func fetchGames() { + NetworkManager.shared.fetchGames { fetchedGames, error in + if let fetchedGames = fetchedGames { + var updatedGames: [Game] = [] + fetchedGames.indices.forEach { index in + let gameData = fetchedGames[index] + let game = Game(game: gameData) + + game.fetchAndUpdateOpponent(opponentId: gameData.opponentId) { updatedGame in + + // append the game only if it is upcoming/live + // TODO: How to determine whether it's live now + let now = Date() + let twoHours: TimeInterval = 2 * 60 * 60 + 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 + + if isLive { + updatedGames.insert(updatedGame, at: 0) + } else if isUpcoming { + updatedGames.append(updatedGame) + } else if isFinishedToday { + updatedGames.append(updatedGame) + } + + if index == fetchedGames.count - 1 { + self.games = updatedGames + self.allGames = updatedGames + self.upcomingGames = Array(allGames.prefix(3)) + } + } + } + } + else if let error = error { + self.errorMessage = error.localizedDescription + print("Error in fetchGames: \(self.errorMessage ?? "Unknown error")") + } + } + } + + private func filterUpcomingGames() { + let gender: String? + let sport: String? + if selectedSex == .Both { + gender = nil + } else { + gender = selectedSex.filterDescription + } + if selectedSport == .All { + sport = nil + } else { + sport = selectedSport.description + } + NetworkManager.shared.filterUpcomingGames(gender: gender, sport: sport) { filteredGames, error in + if let filteredGames = filteredGames { + var updatedGames: [Game] = [] + + filteredGames.indices.forEach { index in + let gameData = filteredGames[index] + let game = Game(game: gameData) + + game.fetchAndUpdateOpponent(opponentId: gameData.opponentId) { updatedGame in + let now = Date() + let twoHours: TimeInterval = 2 * 60 * 60 + 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 + + if isLive { + updatedGames.insert(updatedGame, at: 0) + } else if isUpcoming { + updatedGames.append(updatedGame) + } else if isFinishedToday { + updatedGames.append(updatedGame) + } + + if index == filteredGames.count - 1 { + self.games = updatedGames + } + } + } + } else if let error = error { + errorMessage = error.localizedDescription + print("Error in filterUpcomingGames: \(errorMessage ?? "Unknown error")") + } + } + } +} + +// MARK: Components +extension HomeView { + private var carousel: some View { + VStack { + Text("Upcoming") + .font(Constants.Fonts.semibold24) + .frame(maxWidth: .infinity, alignment: .leading) // Align to the left + .padding(.top, 24) + + // Carousel + TabView(selection: $selectedCardIndex) { + ForEach(upcomingGames.indices, id: \.self) { index in + UpcomingCard(game: upcomingGames[index]) + .tag(index) + } + } + .frame(height: 220) + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic)) + + HStack(spacing: 32) { + ForEach(0..<3, id: \.self) { index in + Circle() + .fill(index == selectedCardIndex ? Constants.Colors.primary_red : Constants.Colors.unselected) + .frame(width: 10, height: 10) + } + } + } + .padding(.bottom, 24) + } + + private var genderSelector: some View { + PickerView(selectedSex: $selectedSex, selectedIndex: 0) + .padding(.bottom, 12) + } + + private var sportSelector: some View { + ScrollView (.horizontal, showsIndicators: false) { + HStack { + ForEach(Sport.allCases) { sport in + Button { + selectedSport = sport + } label: { + FilterTile(sport: sport, selected: sport == selectedSport) + } + } + } + } + } + + private var filters: some View { + // Sex selector + // TODO: full-width to fit the screen + VStack { + PickerView(selectedSex: $selectedSex, selectedIndex: 0) + .padding(.bottom, 12) + + // Sport selector + ScrollView (.horizontal, showsIndicators: false) { + HStack { + ForEach(Sport.allCases) { sport in + Button { + selectedSport = sport + } label: { + FilterTile(sport: sport, selected: sport == selectedSport) + } + } + } + } + .padding(.bottom, 16) + } + } + + private var gameList: some View { + ScrollView (.vertical, showsIndicators: false) { + LazyVStack(spacing: 16) { + ForEach( + games + ) { game in + NavigationLink { + GameView(game: game) + .navigationBarBackButtonHidden() + } label: { + GameTile(game: game) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding(.top, paddingMain) + } + } +} diff --git a/score-ios/Views/MainTabView.swift b/score-ios/Views/MainTabView.swift new file mode 100644 index 0000000..49f692b --- /dev/null +++ b/score-ios/Views/MainTabView.swift @@ -0,0 +1,51 @@ +// +// MainTabView.swift +// score-ios +// +// Created by Hsia Lu wu on 11/16/24. +// + +import SwiftUI + +struct MainTabView: View { + // MARK: Properties + @Binding var selection: Int + @StateObject private var gamesViewModel = GamesViewModel() + + var body: some View { + NavigationStack { + ZStack(alignment: .bottom) { + if (selection == 0) { + HomeView() + .environmentObject(gamesViewModel) + } else { + PastGameView() + .environmentObject(gamesViewModel) + } + } + + HStack { + ForEach(0..<2, id: \.self) { + index in + TabViewIcon(selectionIndex: $selection, itemIndex: index) + .frame(width: 67, height: 45) + if index != 1 { + Spacer() + } + } + } + .ignoresSafeArea(edges: .bottom) + .padding(.leading, 86) + .padding(.trailing, 86) + .padding(.top, 10) + .frame(maxWidth: .infinity) + .background(Constants.Colors.white) + .shadow(radius: 6) + + } + } +} + +#Preview { + MainTabView(selection: .constant(0)) +} diff --git a/score-ios/Views/NoGameView.swift b/score-ios/Views/NoGameView.swift index 58830cd..59b06f2 100644 --- a/score-ios/Views/NoGameView.swift +++ b/score-ios/Views/NoGameView.swift @@ -24,7 +24,6 @@ struct NoGameView: View { .padding(.top, 2) } Spacer() - Spacer() } } } diff --git a/score-ios/Views/PastGameCard.swift b/score-ios/Views/PastGameCard.swift new file mode 100644 index 0000000..5848042 --- /dev/null +++ b/score-ios/Views/PastGameCard.swift @@ -0,0 +1,120 @@ +// +// PastGameCard.swift +// score-ios +// +// Created by Hsia Lu wu on 11/6/24. +// + +import SwiftUI + +struct PastGameCard: View { + var game: Game + var body: some View { + VStack { + banner + Spacer() + information + }.frame(width: 345, height: 192) + .background(Constants.Colors.white) + .clipShape(RoundedRectangle(cornerRadius: 19)) + .background( + RoundedRectangle(cornerRadius: 12) + .stroke(Constants.Colors.gray_border, lineWidth: 1) + .shadow(radius: 5) + ) + .padding(.vertical, 10) + } +} + +#Preview { + PastGameCard(game: Game.dummyData[7]) +} + +// MARK: Components +extension PastGameCard { + private var banner: some View { + HStack { + Spacer() + + Image("Cornell") + .resizable() + .frame(width: 64, height: 64) + + Spacer() + + HStack { + Text(String(game.gameUpdates[game.gameUpdates.count-1].cornellScore)) + .font(Constants.Fonts.title) + .italic() + .foregroundStyle(.white) + + Text(" - ") + .font(Constants.Fonts.title) + .italic() + .foregroundStyle(.white) + + // TODO: Change the blur + Text(String(game.gameUpdates[game.gameUpdates.count-1].opponentScore)) + .font(Constants.Fonts.title) + .blur(radius: 0.5) + .italic() + .foregroundStyle(.white) + } + + Spacer() + + AsyncImage(url: URL(string: game.opponent.image)) {image in + image.resizable() + } placeholder: { + Constants.Colors.gray_icons + } + .frame(width: 64, height: 64) + + Spacer() + } + .padding() + .frame(height: 100) + .background(LinearGradient(gradient: Gradient(colors: [ + Constants.Colors.gradient_red, + Constants.Colors.gradient_blue + ]), startPoint: .leading, endPoint: .trailing)) + } + + private var information: some View { + VStack { + HStack { + AsyncImage(url: URL(string: game.opponent.image)) {image in + image.resizable() + } placeholder: { + Constants.Colors.gray_icons + } + .frame(width: 25, height: 27) + Text(game.opponent.name) + .font(Constants.Fonts.gameTitle) + Spacer() + Image(game.sport.rawValue + "-g") + .resizable() + .frame(width: 30, height: 30) + Image(game.sex.description + "-g") + .resizable() + .frame(width: 25, height: 25) + } + + HStack { + Image("Location-g") + .resizable() + .frame(width: 10, height: 15) + Text("\(game.city), \(game.state)") + .font(Constants.Fonts.gameText) + .foregroundStyle(.gray) + Spacer() + Text(Date.dateToString(date: game.date)) + .font(Constants.Fonts.gameDate) + .foregroundStyle(.gray) + } + } + .padding(.leading, 20) + .padding(.trailing, 20) + .padding(.bottom, 13) + } +} diff --git a/score-ios/Views/PastGameTile.swift b/score-ios/Views/PastGameTile.swift index dafd27e..32df7fb 100644 --- a/score-ios/Views/PastGameTile.swift +++ b/score-ios/Views/PastGameTile.swift @@ -12,20 +12,95 @@ struct PastGameTile: View { var body: some View { - let liveNow: Bool = game.date == Date.currentDate + let corScore = game.gameUpdates[game.gameUpdates.count-1].cornellScore + let oppScore = game.gameUpdates[game.gameUpdates.count-1].opponentScore + let corWon = corScore > oppScore + let tie = corScore == oppScore - VStack { - // Opponent Logo, Opponent Name | Sport Icon, Sex Icon - HStack(spacing: 8) { - HStack(spacing: 8) { - Image(game.opponent.rawValue) - - Text(game.opponent.rawValue) - .font(Constants.Fonts.gameTitle) - } .padding(.leading, 20) - - Spacer() + HStack { + // VStack of school names and logos and score + ZStack { + HStack { + Spacer() // Pushes the rectangle to the right side + Spacer() + Rectangle() + .frame(width: 2, height: 70) // Adjust the thickness of the right border here + .foregroundColor(Constants.Colors.gray_liner) // Color of the right border + } + VStack { + // Opponent score + HStack { + AsyncImage(url: URL(string: game.opponent.image)) { image in + image.image?.resizable() + } + .frame(width: 20, height: 20) + + Text(game.opponent.name) + .font(Constants.Fonts.gameTitle) + .lineLimit(1) + + Spacer() + + // Opponent Score with Arrow + if corWon { + Text(String(oppScore)) + .foregroundStyle(Constants.Colors.gray_text) + .font(Constants.Fonts.medium18) + } else if !tie { + HStack { + Text(String(oppScore)) + .font(Constants.Fonts.semibold18) + Image("pastGame_arrow_back") + .resizable() + .frame(width: 11, height: 14) + } + .offset(x: 20) + } else { + Text(String(oppScore)) + .font(Constants.Fonts.semibold18) + } + + } + + // Cornell Score + HStack { + Image("Cornell") + .resizable() + .frame(width: 20, height: 20) + + Text("Cornell") + .font(Constants.Fonts.gameTitle) + + Spacer() + + // Cornell Score with Arrow + if corWon { + HStack { + Text(String(corScore)) + .font(Constants.Fonts.semibold18) + Image("pastGame_arrow_back") + .resizable() + .frame(width: 11, height: 14) + } + .offset(x: 20) + } else { + Text(String(corScore)) + .foregroundStyle(Constants.Colors.gray_text) + .font(Constants.Fonts.medium18) + } + } + } + .padding(.leading, 16) + .padding(.trailing, 24) + .frame(maxWidth: .infinity) + } + + // arrow + + + // game info: sport, sex, time + VStack { HStack(spacing: 8) { // Sport icon // TODO: frame 24*24 @@ -49,36 +124,17 @@ struct PastGameTile: View { } } .padding(.trailing, 20) - } - - // Location Icon, City, State | Date - HStack { - HStack (spacing: 4) { - Image(Constants.Icons.locationIcon) - .resizable() - .renderingMode(/*@START_MENU_TOKEN@*/.template/*@END_MENU_TOKEN@*/) - .frame(width: 20, height: 20) - .foregroundStyle(Constants.Colors.iconGray) - Text("\(game.city), \(game.state)") - .font(Constants.Fonts.gameText) - .foregroundStyle(Constants.Colors.gray_text) - } .padding(.leading, 20) + .padding(.bottom, 22) - Spacer() - - // TODO: Live Status / Date - if (liveNow) { - - } else { - HStack { - Text(Date.dateToString(date: game.date)) - .font(Constants.Fonts.gameDate) - .foregroundStyle(Constants.Colors.gray_text) - .padding(.trailing, 20) - } + HStack { + Text(Date.dateToString(date: game.date)) + .font(Constants.Fonts.gameDate) + .foregroundStyle(Constants.Colors.gray_text) + .padding(.trailing, 20) } } - } + .padding(.leading, 24) + } .frame(width: 345, height: 96) .background(Constants.Colors.white) .clipShape(RoundedRectangle(cornerRadius: 12)) diff --git a/score-ios/Views/PastGameView.swift b/score-ios/Views/PastGameView.swift index 048ff9f..fd622b2 100644 --- a/score-ios/Views/PastGameView.swift +++ b/score-ios/Views/PastGameView.swift @@ -8,8 +8,58 @@ import SwiftUI struct PastGameView: View { + // State variables + @State private var selectedSex : Sex = .Both + @State private var selectedSport : Sport = .All + var paddingMain : CGFloat = 20 + @State private var selectedCardIndex: Int = 0 + @State private var games: [Game] = [] + @State private var allGames: [Game] = [] + @State private var errorMessage: String? + @State private var pastGames: [Game] = [] + @EnvironmentObject var viewModel: GamesViewModel + + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + NavigationView { + VStack(spacing: 0) { + carousel + + VStack { + Text("All Scores") + .font(Constants.Fonts.semibold24) + .frame(maxWidth: .infinity, alignment: .leading) // Align to the left + + genderSelector + .frame(maxWidth: .infinity, alignment: .center) + sportSelector + } + .padding(.bottom, 16) + + // Seperator line + Divider() + .background(.clear) + + gameList + .overlay { + if (games.isEmpty) { + NoGameView() + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.leading, paddingMain) + .padding(.trailing, paddingMain) + } + .onAppear { + fetchPastGames() + } + .onChange(of: selectedSport) { + filterPastGames() + } + .onChange(of: selectedSex) { + filterPastGames() + } } } @@ -17,9 +67,183 @@ struct PastGameView: View { PastGameView() } +// MARK: Functions +extension PastGameView { + private func fetchPastGames() { + NetworkManager.shared.fetchGames { fetchedGames, error in + if let fetchedGames = fetchedGames { + var updatedGames: [Game] = [] + let dispatchGroup = DispatchGroup() + + fetchedGames.indices.forEach { index in + let gameData = fetchedGames[index] + let game = Game(game: gameData) + + game.fetchAndUpdateOpponent(opponentId: gameData.opponentId) { updatedGame in + + // append the game only if it is upcoming/live + // TODO: How to determine whether it's live now + let now = Date() + let calendar = Calendar.current + let startOfToday = calendar.startOfDay(for: now) + + let isFinishedByToday = game.date < startOfToday + + if isFinishedByToday { + if (!updatedGame.gameUpdates.isEmpty) { + updatedGames.append(updatedGame) + } + } + + if index == fetchedGames.count - 1 { + self.games = updatedGames + self.allGames = updatedGames + self.pastGames = Array(allGames.prefix(3)) + } + } + } + } + else if let error = error { + self.errorMessage = error.localizedDescription + print("Error in fetchGames: \(self.errorMessage ?? "Unknown error")") + } + } + } + + private func filterPastGames() { + let gender: String? + let sport: String? + if selectedSex == .Both { + gender = nil + } else { + gender = selectedSex.filterDescription + } + if selectedSport == .All { + sport = nil + } else { + sport = selectedSport.description + } + NetworkManager.shared.filterUpcomingGames(gender: gender, sport: sport) { filteredGames, error in + if let filteredGames = filteredGames { + var updatedGames: [Game] = [] + + filteredGames.indices.forEach { index in + let gameData = filteredGames[index] + let game = Game(game: gameData) + + game.fetchAndUpdateOpponent(opponentId: gameData.opponentId) { updatedGame in + let now = Date() + let calendar = Calendar.current + let startOfToday = calendar.startOfDay(for: now) + + let isFinishedByToday = game.date < startOfToday + + if isFinishedByToday { + if(!updatedGame.gameUpdates.isEmpty) { + updatedGames.append(updatedGame) + } + } + + if (index == filteredGames.count - 1) { + self.games = updatedGames + } + } + } + } else if let error = error { + errorMessage = error.localizedDescription + print("Error in filterPastGames: \(errorMessage ?? "Unknown error")") + } + } + } +} + // MARK: Components extension PastGameView { - // Past Game Card + private var carousel: some View { + VStack { + Text("Latest") + .font(Constants.Fonts.semibold24) + .frame(maxWidth: .infinity, alignment: .leading) // Align to the left + .padding(.top, 24) + + // Carousel + TabView(selection: $selectedCardIndex) { + ForEach(pastGames.indices, id: \.self) { index in + PastGameCard(game: pastGames[index]) + .tag(index) + } + } + .frame(height: 220) + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic)) + + HStack(spacing: 32) { + ForEach(0..<3, id: \.self) { index in + Circle() + .fill(index == selectedCardIndex ? Constants.Colors.primary_red : Constants.Colors.unselected) + .frame(width: 10, height: 10) + } + } + } + .padding(.bottom, 24) + } + + private var genderSelector: some View { + PickerView(selectedSex: $selectedSex, selectedIndex: 0) + .padding(.bottom, 12) + } + private var sportSelector: some View { + ScrollView (.horizontal, showsIndicators: false) { + HStack { + ForEach(Sport.allCases) { sport in + Button { + selectedSport = sport + } label: { + FilterTile(sport: sport, selected: sport == selectedSport) + } + } + } + } + } + private var filters: some View { + // Sex selector + // TODO: full-width to fit the screen + VStack { + PickerView(selectedSex: $selectedSex, selectedIndex: 0) + .padding(.bottom, 12) + + // Sport selector + ScrollView (.horizontal, showsIndicators: false) { + HStack { + ForEach(Sport.allCases) { sport in + Button { + selectedSport = sport + } label: { + FilterTile(sport: sport, selected: sport == selectedSport) + } + } + } + } + .padding(.bottom, 16) + } + } + + private var gameList: some View { + ScrollView (.vertical, showsIndicators: false) { + LazyVStack(spacing: 16) { + ForEach( + games + ) { game in + NavigationLink { + GameView(game: game) + .navigationBarBackButtonHidden() + } label: { + PastGameTile(game: game) + } + .buttonStyle(PlainButtonStyle()) + } + }.padding(.top, paddingMain) + } + } } diff --git a/score-ios/Views/PickerView.swift b/score-ios/Views/PickerView.swift index 7130dc5..dd3e73b 100644 --- a/score-ios/Views/PickerView.swift +++ b/score-ios/Views/PickerView.swift @@ -9,7 +9,7 @@ import SwiftUI /// Custom PickerView code heavily inspired from https://www.reddit.com/r/SwiftUI/comments/qonfey/how_do_i_get_a_picker_that_looks_like_this_very/ -private var buttonWidth : CGFloat = 100 +private var buttonWidth : CGFloat = 111 private var buttonHeight : CGFloat = 37 struct PickerView: View { @@ -17,13 +17,18 @@ struct PickerView: View { @State var selectedIndex: Int = 0 var body: some View { + let offsetScalar: Int = selectedIndex == 0 ? -1 : selectedIndex + ZStack (alignment: .leading) { - Capsule() - .fill(Constants.Colors.selected) - .frame(width: buttonWidth, height: buttonHeight) - .offset(x: 6 + CGFloat(selectedIndex) * 99) - .animation(.spring(), value: selectedSex) + GeometryReader { geometry in + Capsule() + .fill(Constants.Colors.selected) + .frame(width: buttonWidth, height: buttonHeight) + .offset(x: CGFloat(selectedIndex) * (geometry.size.width / CGFloat(Sex.allCases.count)) - CGFloat(5 * (offsetScalar))) + .animation(.spring(), value: selectedSex) + } + .frame(height: buttonHeight) // Options HStack (spacing: 0) { @@ -51,6 +56,7 @@ struct PickerView: View { .overlay( Capsule() .stroke(Constants.Colors.gray_liner, lineWidth: 2) + .frame(width: 345, height: 49) ) } } diff --git a/score-ios/Views/ProfileViewViewModel.swift b/score-ios/Views/ProfileViewViewModel.swift new file mode 100644 index 0000000..84fcc3e --- /dev/null +++ b/score-ios/Views/ProfileViewViewModel.swift @@ -0,0 +1,8 @@ +// +// ProfileViewViewModel.swift +// All In +// +// Created by Hsia Lu wu on 11/3/24. +// + +import Foundation diff --git a/score-ios/Views/ScoreApp.swift b/score-ios/Views/ScoreApp.swift index 1d09999..7803930 100644 --- a/score-ios/Views/ScoreApp.swift +++ b/score-ios/Views/ScoreApp.swift @@ -6,63 +6,30 @@ // import SwiftUI +import GameAPI /// Main View of the app struct ContentView: View { + @State private var selectedTab: Int = 0 + @State private var games: [GamesQuery.Data.Game] = [] + @State private var errorMessage: String? - // State variables - @State private var selectedSex : Sex = .Both - @State private var selectedSport : Sport = .All - var paddingMain : CGFloat = 20 - @State private var selectedCardIndex: Int = 0 - @State private var games: [Game] = [] - @State private var upcomingGames: [Game] = Array(Game.dummyData.prefix(3)) - // Main view var body: some View { - NavigationView { - VStack(spacing: 0) { - carousel - - Text("Game Schedule") - .font(Constants.Fonts.h1) - .frame(maxWidth: .infinity, alignment: .leading) // Align to the left - .padding(.leading, 20) - .padding(.bottom, 16) - - filters - - // Seperator line - Divider() - .background(.clear) - - // List of games - if (games.isEmpty) { - // make this a separate view - NoGameView() - } else { - gameList - } - } - .padding(.leading, paddingMain) - .padding(.trailing, paddingMain) - .edgesIgnoringSafeArea(.bottom) - } - .onChange(of: selectedSport) { - filterGames() - } - .onChange(of: selectedSex) { - filterGames() - } - } - - func filterGames() { - games = Game.dummyData.filter({(selectedSex == .Both || $0.sex == selectedSex) && (selectedSport == .All || $0.sport == selectedSport)}) + MainTabView(selection: $selectedTab) } } #Preview { - ContentView() + StateWrapper() +} + +struct StateWrapper: View { + @State private var selectedTab: Int = 0 + + var body: some View { + MainTabView(selection: $selectedTab) + } } // MARK: Components diff --git a/score-ios/Views/ScoringSummary.swift b/score-ios/Views/ScoringSummary.swift new file mode 100644 index 0000000..8437902 --- /dev/null +++ b/score-ios/Views/ScoringSummary.swift @@ -0,0 +1,100 @@ +// +// ScoringSummary.swift +// score-ios +// +// Created by Daniel Chuang on 12/4/24. +// + +import SwiftUI + +struct ScoringSummary : View { + + @Environment(\.presentationMode) var presentationMode + var game: Game + + var body : some View { + + ScrollView (.vertical) { + ForEach(game.gameUpdates.indices, id: \.self) { i in + if game.gameUpdates[i].isCornell { + ScoringUpdateCell(update: game.gameUpdates[i], img: "Cornell") + } else { + ScoringUpdateCell(update: game.gameUpdates[i], img: game.opponent.image) + } + + if i < game.gameUpdates.count - 1 { + Divider() + } + } + } + .toolbar { + ToolbarItem(placement: .principal) { + Text("Scoring Summary") + .font(.system(size: 27, weight: .regular)) + } + ToolbarItem(placement: .navigationBarLeading) { + Button { + presentationMode.wrappedValue.dismiss() + } label: { + Image("arrow_back_ios") + .resizable() + .frame(width: 9.87, height: 18.57) + } + } + } + .navigationBarBackButtonHidden() + .toolbarBackground(Color.white, for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + } +} + +struct ScoringUpdateCell : View { + var update: GameUpdate + var img: String + + var body : some View { + HStack { + if update.isCornell { + Image("Cornell") + .resizable().frame(width: 32, height: 32) + } else { + AsyncImage(url: URL(string: img)) {image in + image.resizable().frame(width: 32, height: 32) + } placeholder: { + Constants.Colors.gray_icons.frame(width: 32, height: 32) + } + } + + Spacer() + + VStack { + Text("\("0:00") - \(ordinalNumberString(for: update.timestamp))") + .font(Constants.Fonts.regular14) + + if update.isCornell { + Text("**\(update.cornellScore)** - \(update.opponentScore)") + .font(Constants.Fonts.regular14) + } else { + Text("\(update.cornellScore) - **\(update.opponentScore)**") + .font(Constants.Fonts.regular14) + } + } + .frame(width:72) + + Spacer() + + Text(update.description) + .font(Constants.Fonts.regular14) + .frame(width: 217) + + } + .padding(.leading, 24) + .padding(.trailing, 24) + .padding(.top, 12) + .padding(.bottom, 12) + } +} + +#Preview { + ScoringSummary(game: Game.dummyData[0]) +} diff --git a/score-ios/Views/TabViewIcon.swift b/score-ios/Views/TabViewIcon.swift new file mode 100644 index 0000000..799dd51 --- /dev/null +++ b/score-ios/Views/TabViewIcon.swift @@ -0,0 +1,37 @@ +// +// TabViewIcon.swift +// score-ios +// +// Created by Hsia Lu wu on 11/16/24. +// + +import SwiftUI + +struct TabViewIcon: View { + // MARK: - Properties + + @Binding var selectionIndex: Int + + let itemIndex: Int + private let tabItems = ["schedule", "scoreboard"] + + var body: some View { + Button { + selectionIndex = itemIndex + } label: { + VStack { + Image(itemIndex == selectionIndex ? "\(tabItems[itemIndex])-selected" : tabItems[itemIndex]) + .resizable() + .frame(width: 28, height: 28) + .tint(Constants.Colors.gray_icons) + Text(itemIndex == 0 ? "Schedule" : "Scores") + .font(Constants.Fonts.buttonLabel) + .foregroundStyle(itemIndex == selectionIndex ? Constants.Colors.primary_red : Constants.Colors.unselectedText) + } + } + } +} + +#Preview { + TabViewIcon(selectionIndex: .constant(0), itemIndex: 0) +} diff --git a/score-ios/Views/UpcomingCard.swift b/score-ios/Views/UpcomingCard.swift index 02036f1..84db119 100644 --- a/score-ios/Views/UpcomingCard.swift +++ b/score-ios/Views/UpcomingCard.swift @@ -44,16 +44,18 @@ extension UpcomingCard { Spacer() Image("Cornell") .resizable() - .frame(width: 50, height: 52) + .frame(width: 64, height: 64) Spacer() Text("VS") .font(Constants.Fonts.title) .italic() .foregroundStyle(.white) Spacer() - Image("penn_logo") - .resizable() - .frame(width: 50, height: 58) + AsyncImage(url: URL(string: game.opponent.image)) { + image in + image.image?.resizable() + } + .frame(width: 64, height: 64) Spacer() } .padding() @@ -67,10 +69,13 @@ extension UpcomingCard { private var information: some View { VStack { HStack { - Image("penn_logo") - .resizable() - .frame(width: 24, height: 30) - Text(game.opponent.rawValue) + AsyncImage(url: URL(string: game.opponent.image)) {image in + image.resizable() + } placeholder: { + Constants.Colors.gray_icons + } + .frame(width: 24, height: 24) + Text(game.opponent.name) .font(Constants.Fonts.gameTitle) Spacer() Image(game.sport.rawValue + "-g")