diff --git a/score-ios.xcodeproj/project.pbxproj b/score-ios.xcodeproj/project.pbxproj index b90ba3d..fb89448 100644 --- a/score-ios.xcodeproj/project.pbxproj +++ b/score-ios.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 1C87865D2D8CD76900EBDF74 /* TrailingFadeGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C87865C2D8CD76900EBDF74 /* TrailingFadeGradient.swift */; }; 1C87865F2D8CDADC00EBDF74 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C87865E2D8CDADC00EBDF74 /* String+Extension.swift */; }; + 2384C7B81B22428D94240957 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840304A20FA141C291346BA8 /* Highlight.swift */; }; 2C1375BD2E722CB70089EBC7 /* GameAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 2C1375BC2E722CB70089EBC7 /* GameAPI */; }; 2C1375BF2E7230250089EBC7 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 2C1375BE2E7230250089EBC7 /* FirebaseAnalytics */; }; 2C1375C12E7230250089EBC7 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = 2C1375C02E7230250089EBC7 /* FirebaseCore */; }; @@ -16,6 +17,17 @@ 2C1375C52E7230630089EBC7 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 2C1375C42E7230630089EBC7 /* FirebaseAnalytics */; }; 2C1375C72E7230630089EBC7 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = 2C1375C62E7230630089EBC7 /* FirebaseCore */; }; 2C1375CB2E7233390089EBC7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2C1375CA2E7233390089EBC7 /* GoogleService-Info.plist */; }; + 64005CCECEAC4FD4BA8F51D2 /* YouTubeVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3E7BD70D304BE7A01DB499 /* YouTubeVideo.swift */; }; + 76101F6A2E981E3E006D6EDD /* DetailedHighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76101F692E981E37006D6EDD /* DetailedHighlightView.swift */; }; + 7626AD682E973D9B002149CD /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7626AD672E973D9B002149CD /* SearchView.swift */; }; + 7626AD692E973D9B002149CD /* HighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7626AD642E973D9B002149CD /* HighlightView.swift */; }; + 7626AD6A2E973D9B002149CD /* HighlightTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7626AD632E973D9B002149CD /* HighlightTile.swift */; }; + 7626AD6B2E973D9B002149CD /* HighlightTileArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7626AD652E973D9B002149CD /* HighlightTileArticle.swift */; }; + 7626AD6C2E973D9B002149CD /* HighlightTileVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7626AD662E973D9B002149CD /* HighlightTileVideo.swift */; }; + 7626AD6F2E973E08002149CD /* View+CornerRadius.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7626AD6E2E973E08002149CD /* View+CornerRadius.swift */; }; + 76D998E92E9F1AF900713EE5 /* SearchViewFullScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76D998E82E9F1AF300713EE5 /* SearchViewFullScreen.swift */; }; + 76E679212E9FF6A100C39132 /* NoHighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E679202E9FF69C00C39132 /* NoHighlightView.swift */; }; + B136701ECD164EE9AC64667F /* Article.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CB4DBAE237D47AB882D4EBC /* Article.swift */; }; CE335CD32C922E8D0037F572 /* PrimaryColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE335CD22C922E8D0037F572 /* PrimaryColors.swift */; }; CE335CD52C922ECB0037F572 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE335CD42C922ECB0037F572 /* Constants.swift */; }; CE335CD72C922F390037F572 /* Dates.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE335CD62C922F390037F572 /* Dates.swift */; }; @@ -116,6 +128,18 @@ 1C87865C2D8CD76900EBDF74 /* TrailingFadeGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrailingFadeGradient.swift; sourceTree = ""; }; 1C87865E2D8CDADC00EBDF74 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 2C1375CA2E7233390089EBC7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 6CB4DBAE237D47AB882D4EBC /* Article.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Article.swift; sourceTree = ""; }; + 6F3E7BD70D304BE7A01DB499 /* YouTubeVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeVideo.swift; sourceTree = ""; }; + 76101F692E981E37006D6EDD /* DetailedHighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailedHighlightView.swift; sourceTree = ""; }; + 7626AD632E973D9B002149CD /* HighlightTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightTile.swift; sourceTree = ""; }; + 7626AD642E973D9B002149CD /* HighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightView.swift; sourceTree = ""; }; + 7626AD652E973D9B002149CD /* HighlightTileArticle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightTileArticle.swift; sourceTree = ""; }; + 7626AD662E973D9B002149CD /* HighlightTileVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightTileVideo.swift; sourceTree = ""; }; + 7626AD672E973D9B002149CD /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; + 7626AD6E2E973E08002149CD /* View+CornerRadius.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+CornerRadius.swift"; sourceTree = ""; }; + 76D998E82E9F1AF300713EE5 /* SearchViewFullScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewFullScreen.swift; sourceTree = ""; }; + 76E679202E9FF69C00C39132 /* NoHighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoHighlightView.swift; sourceTree = ""; }; + 840304A20FA141C291346BA8 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = ""; }; CE335CD22C922E8D0037F572 /* PrimaryColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryColors.swift; sourceTree = ""; }; CE335CD42C922ECB0037F572 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; CE335CD62C922F390037F572 /* Dates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dates.swift; sourceTree = ""; }; @@ -252,6 +276,9 @@ CE8ED4FD2D6BF49E00A274DE /* GameUpdate.swift */, CE8ED4FF2D6BF4BE00A274DE /* BoxScoreUpdate.swift */, CE8ED5012D6BF4E800A274DE /* TimeUpdate.swift */, + 6CB4DBAE237D47AB882D4EBC /* Article.swift */, + 6F3E7BD70D304BE7A01DB499 /* YouTubeVideo.swift */, + 840304A20FA141C291346BA8 /* Highlight.swift */, ); path = Models; sourceTree = ""; @@ -271,6 +298,7 @@ 1C87865B2D8CD73C00EBDF74 /* ViewModifiers */, CE335CD22C922E8D0037F572 /* PrimaryColors.swift */, CE335CD42C922ECB0037F572 /* Constants.swift */, + 7626AD6E2E973E08002149CD /* View+CornerRadius.swift */, CE335CD62C922F390037F572 /* Dates.swift */, CE528FA32C9653C200C238B5 /* Error.swift */, CE3C9C422D011A23008BFB4C /* OrdinalSuffix.swift */, @@ -402,6 +430,7 @@ CE725D3D2C89120200386943 /* ScoreApp.swift */, D86347DE2CE98B3C003DD8F6 /* MainTabView.swift */, D86347E02CE98D37003DD8F6 /* TabViewIcon.swift */, + 76D998E82E9F1AF300713EE5 /* SearchViewFullScreen.swift */, CE725D3B2C89120200386943 /* Home.swift */, ); path = MainViews; @@ -412,11 +441,13 @@ children = ( D858020F2D6C38670075B036 /* DynamicScoreBox.swift */, D83EE8852CC9917C008B693C /* ScoreSummaryTile.swift */, + 76101F692E981E37006D6EDD /* DetailedHighlightView.swift */, D89461182CBF393B0010C532 /* UpcomingCard.swift */, CE7666862D5D108E00659A3B /* ScoringUpdateCell.swift */, D86347AE2CDBD2F3003DD8F6 /* PastGameCard.swift */, CE528FF62C979DA000C238B5 /* GameView.swift */, D836AD912CB62C8800BD1545 /* NoGameView.swift */, + 76E679202E9FF69C00C39132 /* NoHighlightView.swift */, D87882272CC060FC00421F67 /* GameDetailedScoreView.swift */, CE3C9C402D010177008BFB4C /* ScoringSummary.swift */, ); @@ -428,6 +459,11 @@ children = ( CE528FA12C9651C800C238B5 /* UpcomingGameTile.swift */, D86347B02CDBFF7C003DD8F6 /* UpcomingGamesView.swift */, + 7626AD632E973D9B002149CD /* HighlightTile.swift */, + 7626AD642E973D9B002149CD /* HighlightView.swift */, + 7626AD652E973D9B002149CD /* HighlightTileArticle.swift */, + 7626AD662E973D9B002149CD /* HighlightTileVideo.swift */, + 7626AD672E973D9B002149CD /* SearchView.swift */, D8B1C9D22CD2D20A0095E563 /* PastGameTile.swift */, D8B1C9D02CD2CE3C0095E563 /* PastGamesView.swift */, CE528F9F2C96420700C238B5 /* PickerView.swift */, @@ -594,8 +630,8 @@ mainGroup = CE725D2F2C89120100386943; packageReferences = ( D89102012CED68D9004CE226 /* XCRemoteSwiftPackageReference "apollo-ios" */, - FD602E2D2DC11D4A002711BE /* XCLocalSwiftPackageReference "GameAPI" */, D0A904F32E8DDD990008194B /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, + 76101F682E9743EC006D6EDD /* XCLocalSwiftPackageReference "gameAPI" */, ); productRefGroup = CE725D392C89120200386943 /* Products */; projectDirPath = ""; @@ -688,14 +724,25 @@ D89102132CF10CA9004CE226 /* NetworkManager.swift in Sources */, CE725D3E2C89120200386943 /* ScoreApp.swift in Sources */, D836AD922CB62C8800BD1545 /* NoGameView.swift in Sources */, + 7626AD682E973D9B002149CD /* SearchView.swift in Sources */, + 7626AD692E973D9B002149CD /* HighlightView.swift in Sources */, + 76101F6A2E981E3E006D6EDD /* DetailedHighlightView.swift in Sources */, + 7626AD6A2E973D9B002149CD /* HighlightTile.swift in Sources */, + 7626AD6B2E973D9B002149CD /* HighlightTileArticle.swift in Sources */, + 7626AD6C2E973D9B002149CD /* HighlightTileVideo.swift in Sources */, CE528FF52C9798AC00C238B5 /* FilterTile.swift in Sources */, CE7666872D5D108E00659A3B /* ScoringUpdateCell.swift in Sources */, + 76D998E92E9F1AF900713EE5 /* SearchViewFullScreen.swift in Sources */, D85802102D6C38700075B036 /* DynamicScoreBox.swift in Sources */, D8B1C9D32CD2D20A0095E563 /* PastGameTile.swift in Sources */, + 7626AD6F2E973E08002149CD /* View+CornerRadius.swift in Sources */, CE528FA22C9651C800C238B5 /* UpcomingGameTile.swift in Sources */, FD5A38DD2D8F30CC00CF5E30 /* GameErrorView.swift in Sources */, CEA25A932D75279A00B9837A /* ScoreEnvironment.swift in Sources */, FD5A38DB2D8F2BDD00CF5E30 /* GameLoadingView.swift in Sources */, + B136701ECD164EE9AC64667F /* Article.swift in Sources */, + 64005CCECEAC4FD4BA8F51D2 /* YouTubeVideo.swift in Sources */, + 2384C7B81B22428D94240957 /* Highlight.swift in Sources */, CE8ED4FC2D6BF47C00A274DE /* DummyData.swift in Sources */, CE335CD92C9244230037F572 /* Game.swift in Sources */, D89102102CF0EBA4004CE226 /* DataCoordinator.swift in Sources */, @@ -718,6 +765,7 @@ FD27F4232DC0A68900CC172E /* GamesCacheManager.swift in Sources */, CE725D3C2C89120200386943 /* Home.swift in Sources */, FD5A38DF2D8F3E1400CF5E30 /* ShimmerModifier.swift in Sources */, + 76E679212E9FF6A100C39132 /* NoHighlightView.swift in Sources */, CE528FA02C96420700C238B5 /* PickerView.swift in Sources */, CE528FA42C9653C200C238B5 /* Error.swift in Sources */, CE335CD52C922ECB0037F572 /* Constants.swift in Sources */, @@ -1067,9 +1115,9 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - FD602E2D2DC11D4A002711BE /* XCLocalSwiftPackageReference "GameAPI" */ = { + 76101F682E9743EC006D6EDD /* XCLocalSwiftPackageReference "gameAPI" */ = { isa = XCLocalSwiftPackageReference; - relativePath = GameAPI; + relativePath = gameAPI; }; /* End XCLocalSwiftPackageReference section */ 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..61e0739 --- /dev/null +++ b/score-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,141 @@ +{ + "originHash" : "2dde18dff3dee93fc39b51fdffcd81274cf0142e3c64fd694e72622d60b64d97", + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "apollo-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apollographql/apollo-ios.git", + "state" : { + "revision" : "5dee1f01003e8b0fec864d47b0724cd0d441ae35", + "version" : "1.24.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "541ac342abead313f2ce0ccf33278962b5c1e43c", + "version" : "12.4.0" + } + }, + { + "identity" : "google-ads-on-device-conversion-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", + "state" : { + "revision" : "2ba031f43ef88a7f6631c84d23794eb99751e891", + "version" : "3.1.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "52713644ce2831bb687ded4aefd5e5c9f15565c5", + "version" : "12.4.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", + "version" : "8.1.0" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", + "version" : "1.69.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "fb7f2740b1570d2f7599c6bb9531bf4fad6974b7", + "version" : "5.0.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "c6fe6442e6a64250495669325044052e113e990c", + "version" : "1.32.0" + } + } + ], + "version" : 3 +} diff --git a/score-ios/Models/Article.swift b/score-ios/Models/Article.swift new file mode 100644 index 0000000..b5387c0 --- /dev/null +++ b/score-ios/Models/Article.swift @@ -0,0 +1,69 @@ +// +// Article.swift +// score-ios +// +// Created by Zain Bilal on 10/8/25. +// + +import Foundation + +struct Article: Identifiable { + var id: String + var title: String + var summary: String + var image: String + var url: String + var source: String + var publishedAt: String + + var formattedDate: String { + if let date = ISO8601DateFormatter().date(from: publishedAt) { + let formatter = DateFormatter() + formatter.dateFormat = "MM/dd" + return formatter.string(from: date) + } + return publishedAt + } +} + +// MARK: - Dummy Data +extension Article { + static let dummyData: [Article] = [ + Article( + id: "1", + title: "Cornell Upsets Rival in Thrilling Overtime Victory", + summary: "Cornell's offense exploded late in the fourth quarter to secure a dramatic win.", + image: "https://snworksceo.imgix.net/cds/2f1df221-010c-4a5b-94cc-ec7a100b7aa1.sized-1000x1000.jpg?w=1000&dpr=2", + url: "https://cornellbigred.com/news/2025/10/08/article", + source: "Cornell Daily Sun", + publishedAt: "2025-10-08T00:00:00Z" + ), + Article( + id: "2", + title: "Cornell Daily Sun Reports Historic Win", + summary: "Cornell's offense shines in a big win.", + image: "https://snworksceo.imgix.net/cds/2f1df221-010c-4a5b-94cc-ec7a100b7aa1.sized-1000x1000.jpg?w=1000&dpr=2", + url: "https://cornellsun.com/article", + source: "Cornell Daily Sun", + publishedAt: "2025-10-09T00:00:00Z" + ), + Article( + id: "3", + title: "Big Red Basketball Team Advances to Championship", + summary: "Cornell basketball team secures spot in the championship game with dominant performance.", + image: "https://snworksceo.imgix.net/cds/2f1df221-010c-4a5b-94cc-ec7a100b7aa1.sized-1000x1000.jpg?w=1000&dpr=2", + url: "https://cornellsun.com/basketball-championship", + source: "Cornell Daily Sun", + publishedAt: "2025-10-10T00:00:00Z" + ), + Article( + id: "4", + title: "Hockey Team Prepares for Rivalry Game", + summary: "Cornell hockey team gears up for the highly anticipated rivalry matchup.", + image: "https://snworksceo.imgix.net/cds/2f1df221-010c-4a5b-94cc-ec7a100b7aa1.sized-1000x1000.jpg?w=1000&dpr=2", + url: "https://cornellsun.com/hockey-rivalry", + source: "Cornell Daily Sun", + publishedAt: "2025-10-11T00:00:00Z" + ) + ] +} diff --git a/score-ios/Models/GraphQL/schema.graphqls b/score-ios/Models/GraphQL/schema.graphqls index 76c410d..3433c92 100644 --- a/score-ios/Models/GraphQL/schema.graphqls +++ b/score-ios/Models/GraphQL/schema.graphqls @@ -23,6 +23,7 @@ directive @defer( ) on FRAGMENT_SPREAD | INLINE_FRAGMENT type Query { + articles(sportsType: String): [ArticleType] youtubeVideos: [YoutubeVideoType] youtubeVideo(id: String!): YoutubeVideoType games( @@ -33,7 +34,7 @@ type Query { offset: Int = 0 ): [GameType] game(id: String!): GameType - gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!): GameType + gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!, ticketLink: String): GameType gamesBySport(sport: String!): [GameType] gamesByGender(gender: String!): [GameType] gamesBySportGender(sport: String!, gender: String!): [GameType] @@ -42,6 +43,25 @@ type Query { teamByName(name: String!): TeamType } +""" +A GraphQL type representing a news article. + +Attributes: + - title: The title of the article + - image: The filename of the article's main image + - sports_type: The specific sport category + - published_at: The publication date + - url: The URL to the full article +""" +type ArticleType { + id: String + title: String! + image: String + sportsType: String! + publishedAt: String! + url: String! +} + """ A GraphQL type representing a YouTube video. @@ -79,6 +99,7 @@ Attributes: - `time`: The time of the game. (optional) - `box_score`: The box score of the game. - `score_breakdown`: The score breakdown of the game. + - `ticket_link`: The ticket link of the game. (optional) """ type GameType { id: String @@ -95,6 +116,7 @@ type GameType { scoreBreakdown: [[String]] team: TeamType utcDate: String + ticketLink: String } """ @@ -143,13 +165,16 @@ type TeamType { type Mutation { """Creates a new game.""" - createGame(boxScore: String, city: String!, date: String!, gender: String!, location: String, opponentId: String!, result: String, scoreBreakdown: String, sport: String!, state: String!, time: String!, utcDate: String): CreateGame + createGame(boxScore: String, city: String!, date: String!, gender: String!, location: String, opponentId: String!, result: String, scoreBreakdown: String, sport: String!, state: String!, ticketLink: String, time: String!, utcDate: String): CreateGame """Creates a new team.""" createTeam(b64Image: String, color: String!, image: String, name: String!): CreateTeam """Creates a new youtube video.""" createYoutubeVideo(b64Thumbnail: String!, description: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo + + """Creates a new article.""" + createArticle(image: String, publishedAt: String!, slug: String!, sportsType: String!, title: String!, url: String!): CreateArticle } type CreateGame { @@ -162,4 +187,8 @@ type CreateTeam { type CreateYoutubeVideo { youtubeVideo: YoutubeVideoType +} + +type CreateArticle { + article: ArticleType } \ No newline at end of file diff --git a/score-ios/Models/Highlight.swift b/score-ios/Models/Highlight.swift new file mode 100644 index 0000000..586309f --- /dev/null +++ b/score-ios/Models/Highlight.swift @@ -0,0 +1,112 @@ +// +// Highlight.swift +// score-ios +// +// Created by Zain Bilal on 10/8/25. +// + +import Foundation + +enum Highlight: Identifiable { + case video(YouTubeVideo) + case article(Article) + + var id: String { + switch self { + case .video(let video): + return video.id + case .article(let article): + return article.id + } + } + + var publishedAt: String { + switch self { + case .video(let video): + return video.publishedAt + case .article(let article): + return article.publishedAt + } + } + + var title: String{ + switch self{ + case .article(let article): + return article.title + case .video(let video): + return video.title + } + } +} + +// MARK: - Dummy Data +extension Highlight { + static let dummyData: [Highlight] = [ + .video( + YouTubeVideo( + id: "QGHb9heJAco", + title: "Cornell Celebrates Coach Mike Schafer '86", + description: "Cornell Celebrates Coach Mike Schafer '86 Narrated by Jeremy Schaap '91.", + thumbnail: "https://i.ytimg.com/vi/QGHb9heJAco/hqdefault.jpg", + b64Thumbnail: nil, + url: "https://youtube.com/watch?v=QGHb9heJAco", + publishedAt: "2025-10-014T00:00:00Z" + ) + ), + .article( + Article( + id: "1", + title: "Cornell Daily Sun Reports Historic Win", + summary: "Cornell's offense shines in a big win.", + image: "https://snworksceo.imgix.net/cds/2f1df221-010c-4a5b-94cc-ec7a100b7aa1.sized-1000x1000.jpg?w=1000&dpr=2", + url: "https://cornellsun.com/article", + source: "Cornell Daily Sun", + publishedAt: "2025-10-14T00:00:00Z" + ) + ), + .video( + YouTubeVideo( + id: "ABC123def", + title: "Cornell Basketball Highlights - Championship Game", + description: "Watch the best moments from Cornell's championship victory.", + thumbnail: "https://i.ytimg.com/vi/ABC123def/hqdefault.jpg", + b64Thumbnail: nil, + url: "https://youtube.com/watch?v=ABC123def", + publishedAt: "2025-10-013T00:00:00Z" + ) + ), + .article( + Article( + id: "2", + title: "Cornell Upsets Rival in Thrilling Overtime Victory", + summary: "Cornell's offense exploded late in the fourth quarter to secure a dramatic win.", + image: "https://snworksceo.imgix.net/cds/2f1df221-010c-4a5b-94cc-ec7a100b7aa1.sized-1000x1000.jpg?w=1000&dpr=2", + url: "https://cornellbigred.com/news/2025/10/08/article", + source: "Cornell Daily Sun", + publishedAt: "2025-10-14T00:00:00Z" + ) + ), + .video( + YouTubeVideo( + id: "XYZ789ghi", + title: "Cornell Hockey Rivalry Game Recap", + description: "Complete recap of the intense rivalry game between Cornell and their arch-rivals.", + thumbnail: "https://i.ytimg.com/vi/XYZ789ghi/hqdefault.jpg", + b64Thumbnail: nil, + url: "https://youtube.com/watch?v=XYZ789ghi", + publishedAt: "2025-10-14T00:00:00Z" + ) + ), + .article( + Article( + id: "3", + title: "Big Red Basketball Team Advances to Championship", + summary: "Cornell basketball team secures spot in the championship game with dominant performance.", + image: "https://snworksceo.imgix.net/cds/2f1df221-010c-4a5b-94cc-ec7a100b7aa1.sized-1000x1000.jpg?w=1000&dpr=2", + url: "https://cornellsun.com/basketball-championship", + source: "Cornell Daily Sun", + publishedAt: "2025-10-13T00:00:00Z" + ) + ) + ] +} diff --git a/score-ios/Models/YouTubeVideo.swift b/score-ios/Models/YouTubeVideo.swift new file mode 100644 index 0000000..e855d28 --- /dev/null +++ b/score-ios/Models/YouTubeVideo.swift @@ -0,0 +1,70 @@ +// +// YoutubeVideo.swift +// score-ios +// +// Created by Zain Bilal on 10/8/25. +// + +import Foundation + +struct YouTubeVideo: Identifiable { + var id: String + var title: String + var description: String + var thumbnail: String + var b64Thumbnail: String? + var url: String + var publishedAt: String + + // Format publishedAt -> MM/dd or similar + var formattedDate: String { + if let date = ISO8601DateFormatter().date(from: publishedAt) { + let formatter = DateFormatter() + formatter.dateFormat = "MM/dd" + return formatter.string(from: date) + } + return publishedAt + } +} + +// MARK: - Dummy Data +extension YouTubeVideo { + static let dummyData: [YouTubeVideo] = [ + YouTubeVideo( + id: "QGHb9heJAco", + title: "Cornell Celebrates Coach Mike Schafer '86", + description: "Cornell Celebrates Coach Mike Schafer '86 Narrated by Jeremy Schaap '91.", + thumbnail: "https://i.ytimg.com/vi/QGHb9heJAco/hqdefault.jpg", + b64Thumbnail: nil, + url: "https://youtube.com/watch?v=QGHb9heJAco", + publishedAt: "2025-10-09T00:00:00Z" + ), + YouTubeVideo( + id: "ABC123def", + title: "Cornell Basketball Highlights - Championship Game", + description: "Watch the best moments from Cornell's championship victory.", + thumbnail: "https://i.ytimg.com/vi/ABC123def/hqdefault.jpg", + b64Thumbnail: nil, + url: "https://youtube.com/watch?v=ABC123def", + publishedAt: "2025-10-08T00:00:00Z" + ), + YouTubeVideo( + id: "XYZ789ghi", + title: "Cornell Hockey Rivalry Game Recap", + description: "Complete recap of the intense rivalry game between Cornell and their arch-rivals.", + thumbnail: "https://i.ytimg.com/vi/XYZ789ghi/hqdefault.jpg", + b64Thumbnail: nil, + url: "https://youtube.com/watch?v=XYZ789ghi", + publishedAt: "2025-10-10T00:00:00Z" + ), + YouTubeVideo( + id: "DEF456jkl", + title: "Cornell Football Season Highlights", + description: "Best plays and moments from Cornell's football season.", + thumbnail: "https://i.ytimg.com/vi/DEF456jkl/hqdefault.jpg", + b64Thumbnail: nil, + url: "https://youtube.com/watch?v=DEF456jkl", + publishedAt: "2025-10-11T00:00:00Z" + ) + ] +} diff --git a/score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/Contents.json new file mode 100644 index 0000000..7f5fffe --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "highlight-selected.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "highlight-selectedx2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "highlight-selectedx3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/highlight-selected.png b/score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/highlight-selected.png new file mode 100644 index 0000000..a696780 Binary files /dev/null and b/score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/highlight-selected.png differ diff --git a/score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/highlight-selectedx2.png b/score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/highlight-selectedx2.png new file mode 100644 index 0000000..5914326 Binary files /dev/null and b/score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/highlight-selectedx2.png differ diff --git a/score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/highlight-selectedx3.png b/score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/highlight-selectedx3.png new file mode 100644 index 0000000..a8c9b65 Binary files /dev/null and b/score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/highlight-selectedx3.png differ diff --git a/score-ios/Resources/Assets.xcassets/highlight.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/highlight.imageset/Contents.json new file mode 100644 index 0000000..a061666 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/highlight.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "highlight.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "highlightx2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "highlightx3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/score-ios/Resources/Assets.xcassets/highlight.imageset/highlight.png b/score-ios/Resources/Assets.xcassets/highlight.imageset/highlight.png new file mode 100644 index 0000000..9d69016 Binary files /dev/null and b/score-ios/Resources/Assets.xcassets/highlight.imageset/highlight.png differ diff --git a/score-ios/Resources/Assets.xcassets/highlight.imageset/highlightx2.png b/score-ios/Resources/Assets.xcassets/highlight.imageset/highlightx2.png new file mode 100644 index 0000000..205b31a Binary files /dev/null and b/score-ios/Resources/Assets.xcassets/highlight.imageset/highlightx2.png differ diff --git a/score-ios/Resources/Assets.xcassets/highlight.imageset/highlightx3.png b/score-ios/Resources/Assets.xcassets/highlight.imageset/highlightx3.png new file mode 100644 index 0000000..8971a50 Binary files /dev/null and b/score-ios/Resources/Assets.xcassets/highlight.imageset/highlightx3.png differ diff --git a/score-ios/Utils/Dates.swift b/score-ios/Utils/Dates.swift index 4018c13..6dd25f4 100644 --- a/score-ios/Utils/Dates.swift +++ b/score-ios/Utils/Dates.swift @@ -38,6 +38,13 @@ extension Date { let calendar = Calendar.current return calendar.date(from: components) ?? Date() } + + // Return true if 'date' is within 'days' from today + static func isWithinPastDays(_ date: Date, days: Int) -> Bool { + guard let pastDate = Calendar.current.date(byAdding: .day, value: -days, to: Date()) else { return false } + + return date >= pastDate && date < Calendar.current.startOfDay(for: Date()) + } static func parseDate(dateString: String, timeString: String) -> Date { // Set up date formatter diff --git a/score-ios/Utils/View+CornerRadius.swift b/score-ios/Utils/View+CornerRadius.swift new file mode 100644 index 0000000..7757a8a --- /dev/null +++ b/score-ios/Utils/View+CornerRadius.swift @@ -0,0 +1,29 @@ +// +// View+CornerRadius.swift +// score-ios +// +// Created by Zain Bilal on 10/3/25. +// + +import SwiftUI + +struct RoundedCorner: Shape { + var radius: CGFloat = .infinity + var corners: UIRectCorner = .allCorners + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius) + ) + + return Path(path.cgPath) + } +} + +extension View { + func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { + clipShape(RoundedCorner(radius: radius, corners: corners)) + } +} diff --git a/score-ios/Views/DetailedViews/DetailedHighlightView.swift b/score-ios/Views/DetailedViews/DetailedHighlightView.swift new file mode 100644 index 0000000..3f9ae06 --- /dev/null +++ b/score-ios/Views/DetailedViews/DetailedHighlightView.swift @@ -0,0 +1,75 @@ +// +// DetailedHighlightView.swift +// score-ios +// +// Created by Zain Bilal on 10/9/25. +// + +import SwiftUI + +struct DetailedHighlightsView: View { + @Environment(\.dismiss) private var dismiss + var title: String + var highlights: [Highlight] + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + + ZStack { + Text(title) + .font(Constants.Fonts.header) + .foregroundStyle(Constants.Colors.black) + + HStack { + Button(action: { dismiss() }) { + Image("arrow_back_ios") + .resizable() + .frame(width: 9.87, height: 18.57) + } + + Spacer() + } + } + .padding(.top, 24) + .padding(.horizontal, 24) + + Divider() + .background(.clear) + + VStack(alignment: .leading, spacing: 0) { + SearchView(highlights: highlights, title: "Search \(title)") + .padding(.horizontal, 24) + .padding(.top, 20) + + SportSelectorView() + .padding(.top, 20) + + VStack{ + ForEach(highlights) { highlight in + HighlightTile(highlight: highlight, width: 360) + .padding(.horizontal, 24) + .padding(.top, 12) + } + } + .padding(.top, 20) + } + + + } + } + // hide default nav bar so only your custom one shows + .navigationBarBackButtonHidden(true) + .navigationBarTitleDisplayMode(.inline) + .safeAreaInset(edge: .bottom) { + Color.clear.frame(height: 200) + } + } +} + +#Preview { + DetailedHighlightsView( + title: "Today", + highlights: Highlight.dummyData + ) +} diff --git a/score-ios/Views/DetailedViews/NoHighlightView.swift b/score-ios/Views/DetailedViews/NoHighlightView.swift new file mode 100644 index 0000000..1143990 --- /dev/null +++ b/score-ios/Views/DetailedViews/NoHighlightView.swift @@ -0,0 +1,39 @@ +// +// NoHighlightView.swift +// score-ios +// +// Created by Zain Bilal on 10/15/25. +// + +import SwiftUI + +struct NoHighlightView: View { + var body: some View { + VStack { + Spacer() + + VStack { + // TODO: make this image better (higher quality and more accurate colors) + Image("highlight") + .resizable() + .frame(width: 100, height: 100, alignment: .center) + .tint(Constants.Colors.gray_icons) + + Text("No results yet.") + .font(Constants.Fonts.bodyBold) + .foregroundStyle(Constants.Colors.gray_text) + + Text("Check back here later!") + .font(Constants.Fonts.caption) + .foregroundStyle(Constants.Colors.gray_text) + } + + Spacer() + } + } +} + +#Preview { + NoHighlightView() +} + diff --git a/score-ios/Views/ListViews/HighlightTile.swift b/score-ios/Views/ListViews/HighlightTile.swift new file mode 100644 index 0000000..2238703 --- /dev/null +++ b/score-ios/Views/ListViews/HighlightTile.swift @@ -0,0 +1,22 @@ +// +// HighlightTile.swift +// score-ios +// +// Created by Zain Bilal on 10/3/25. +// + +import SwiftUI + +struct HighlightTile: View { + var highlight: Highlight + var width: CGFloat + + var body: some View { + switch highlight { + case .video(let video): + HighlightTileVideo(video: video, width:width) + case .article(let article): + HighlightTileArticle(article: article, width:width) + } + } +} diff --git a/score-ios/Views/ListViews/HighlightTileArticle.swift b/score-ios/Views/ListViews/HighlightTileArticle.swift new file mode 100644 index 0000000..f9b693d --- /dev/null +++ b/score-ios/Views/ListViews/HighlightTileArticle.swift @@ -0,0 +1,104 @@ +// +// HightlightTileArticle.swift +// score-ios +// +// Created by Zain Bilal on 10/8/25. +// + +import SwiftUI + +struct HighlightTileArticle: View { + var article: Article + var width: CGFloat + + var body: some View { + if let url = URL(string: article.url) { + Link(destination: url) { + ZStack(alignment: .topLeading) { + // Background Image with dark overlay + AsyncImage(url: URL(string: article.image)) { phase in + switch phase { + case .empty: + Rectangle() + .fill(Constants.Colors.gray_icons.opacity(0.2)) + .overlay(Color.black.opacity(0.60)) // dark tint + case .success(let image): + image + .resizable() + .scaledToFill() + .overlay(Color.black.opacity(0.60)) // dark tint + case .failure(_): + Rectangle() + .fill(Constants.Colors.gray_icons.opacity(0.3)) + .overlay( + Image(systemName: "photo") + .resizable() + .scaledToFit() + .foregroundColor(.white.opacity(0.7)) + .padding() + .overlay(Color.black.opacity(0.60)) // dark tint + ) + @unknown default: + EmptyView() + } + } + .frame(width: width, height: 192) + .clipped() + .cornerRadius(12) + + // Text overlay + VStack(alignment: .leading, spacing: 0) { + // Title at top left + Text(article.title) + .font(.title3) + .fontWeight(.bold) + .foregroundColor(.white) + .lineLimit(3) + .multilineTextAlignment(.leading) + .padding(.top, 12) + .padding(.horizontal, 24) + + Spacer() + + // Source and date at bottom + HStack { + Text(article.source) + .font(.subheadline) + .fontWeight(.bold) + .foregroundColor(Constants.Colors.white) + .underline() + + Image(systemName: "arrow.up.right") + .foregroundStyle(Constants.Colors.white) + .fontWeight(.bold) + + Spacer() + + Text(article.formattedDate) + .font(.caption) + .foregroundColor(.white.opacity(0.9)) + } + .padding([.horizontal, .bottom], 12) + } + .frame(width: width, height: 192, alignment: .topLeading) + + } + .background( + RoundedRectangle(cornerRadius: 12) + .stroke(Constants.Colors.gray_border, lineWidth: 1) + .shadow(radius: 5) + ) + } + } + + + } +} + + +// MARK: - Preview + +#Preview { + HighlightTileArticle(article: Article.dummyData[0], width: 345) +} + diff --git a/score-ios/Views/ListViews/HighlightTileVideo.swift b/score-ios/Views/ListViews/HighlightTileVideo.swift new file mode 100644 index 0000000..aa2cf08 --- /dev/null +++ b/score-ios/Views/ListViews/HighlightTileVideo.swift @@ -0,0 +1,106 @@ +// +// HightlightTileVideo.swift +// score-ios +// +// Created by Zain Bilal on 10/8/25. +// + +import SwiftUI + +struct HighlightTileVideo: View { + var video: YouTubeVideo + var width: CGFloat + + var body: some View { + + if let url = URL(string: video.url) { + Link(destination: url) { + VStack(alignment: .leading, spacing: 1) { + // Thumbnail + AsyncImage(url: URL(string: video.thumbnail)) { phase in + switch phase { + case .empty: + // While loading + Rectangle() + .fill(Constants.Colors.gray_icons.opacity(0.2)) + case .success(let image): + image + .resizable() + .scaledToFill() + case .failure(_): + // If loading fails + Image(systemName: "photo") + .resizable() + .scaledToFit() + .foregroundColor(.gray) + @unknown default: + EmptyView() + } + } + .frame(width: width, height: 117) + .clipped() + .overlay( + HStack(spacing: 2) { + Image(systemName: "play.fill") + .font(.caption2) + + Text("1:25") + .font(.caption) + } + .fontWeight(.heavy) + .foregroundStyle(.white) + .padding(6) + .clipShape(RoundedRectangle(cornerRadius: 4)), alignment: .bottomLeading + ) + .cornerRadius(12, corners: [.topLeft, .topRight]) + + VStack(alignment: .leading, spacing: 8){ + // Title + Text(video.title) + .font(.headline) + .foregroundColor(.black) + .lineLimit(1) + + // Youtube + Date + HStack(spacing: 8) { + if let url = URL(string: video.url) { + Link(destination: url) { + HStack{ + Text("Youtube") + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(Constants.Colors.primary_red) + .underline() + + Image(systemName: "arrow.up.right") + .foregroundStyle(Constants.Colors.primary_red) + .fontWeight(.bold) + } + } + } + + Spacer() + + Text(video.formattedDate) + .font(.caption) + .foregroundColor(.gray) + } + } + .padding(.horizontal, 12.0) + .frame(width: width, height: 75) + } + + .background( + RoundedRectangle(cornerRadius: 12) + .stroke(Constants.Colors.gray_border, lineWidth: 1) + .shadow(radius: 5) + ) + } + } + } +} + + +#Preview { + HighlightTileVideo(video: YouTubeVideo.dummyData[0], width: 241) +} diff --git a/score-ios/Views/ListViews/HighlightView.swift b/score-ios/Views/ListViews/HighlightView.swift new file mode 100644 index 0000000..0ff0810 --- /dev/null +++ b/score-ios/Views/ListViews/HighlightView.swift @@ -0,0 +1,103 @@ +// +// HighlightView.swift +// score-ios +// +// Created by Zain Bilal on 10/4/25. +// + +import SwiftUI + +struct HighlightView: View { + @State var highlights: [Highlight] + + var body: some View { + // Filter highlights + let todayHighlights = highlights.filter { + if let date = Date.fullDateFormatter.date(from: $0.publishedAt) { + return Date.isWithinPastDays(date, days: 1) + } + return false + } + + let pastThreeDaysHighlights = highlights.filter { + guard let date = Date.fullDateFormatter.date(from: $0.publishedAt) else { return false } + return !Date.isWithinPastDays(date, days: 1) && Date.isWithinPastDays(date, days: 3) + } + + ScrollView(showsIndicators: false) { + LazyVStack(alignment: .leading, pinnedViews: [.sectionHeaders]) { + VStack(alignment: .leading, spacing: 4) { + Text("Highlights") + .font(Constants.Fonts.semibold24) + .foregroundStyle(Constants.Colors.black) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 24) + .padding(.horizontal, 24) + + SearchView(highlights: highlights, title: "Search All Highlights") + .padding(.horizontal, 20) + .padding(.top, 12) + + SportSelectorView() + .padding(.horizontal, 20) + .padding(.top, 12) + + if !todayHighlights.isEmpty { + HighlightSectionView(title: "Today", highlights: todayHighlights) + } + + if !pastThreeDaysHighlights.isEmpty { + HighlightSectionView(title: "Past 3 Days", highlights: pastThreeDaysHighlights) + } + } + } + } + } +} + +struct HighlightSectionView: View { + let title: String + let highlights: [Highlight] + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + NavigationLink(destination: DetailedHighlightsView(title: title, highlights: highlights)) { + HStack { + Text(title) + .font(Constants.Fonts.subheader) + .foregroundStyle(Constants.Colors.black) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + Text("\(highlights.count) results") + .font(Constants.Fonts.body) + .foregroundStyle(Constants.Colors.gray_text) + + Image(systemName: "chevron.right") + .font(Constants.Fonts.body) + .foregroundStyle(Constants.Colors.gray_text) + } + .padding(.top, 20) + .padding(.horizontal, 24) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 24) { + ForEach(highlights) { highlight in + HighlightTile(highlight: highlight, width: 241) + } + } + .padding(.top, 20) + .padding(.horizontal, 24) + } + } + } +} + + +// MARK: - Preview + +#Preview { + HighlightView(highlights: Highlight.dummyData) +} diff --git a/score-ios/Views/ListViews/SearchView.swift b/score-ios/Views/ListViews/SearchView.swift new file mode 100644 index 0000000..afd1734 --- /dev/null +++ b/score-ios/Views/ListViews/SearchView.swift @@ -0,0 +1,42 @@ +// +// SearchView.swift +// score-ios +// +// Created by Zain Bilal on 10/5/25. +// + +import SwiftUI + +struct SearchView: View { + @State private var showSearch = false + var highlights: [Highlight] + let title: String + + var body: some View { + Button(action: { showSearch = true }) { + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(Constants.Colors.gray_text) + + Text("Search keywords") + .foregroundColor(Constants.Colors.gray_text) + + Spacer() + } + .padding(8) + .overlay( + RoundedRectangle(cornerRadius: 30) + .stroke(Constants.Colors.gray_border, lineWidth: 1) + ) + } + .buttonStyle(PlainButtonStyle()) + .fullScreenCover(isPresented: $showSearch) { + SearchViewFullScreen(title: title, allHighlights: highlights) + } + } +} + + +#Preview { + SearchView(highlights: Highlight.dummyData, title: "Search All Highlights") +} diff --git a/score-ios/Views/MainViews/MainTabView.swift b/score-ios/Views/MainViews/MainTabView.swift index db936f3..99d9235 100644 --- a/score-ios/Views/MainViews/MainTabView.swift +++ b/score-ios/Views/MainViews/MainTabView.swift @@ -11,36 +11,35 @@ struct MainTabView: View { // MARK: Properties - @Binding var selection: Int + @Binding var selectedTab: MainTab @StateObject private var gamesViewModel = GamesViewModel.shared var body: some View { NavigationStack { ZStack(alignment: .bottom) { - if (selection == 0) { - UpcomingGamesView() - .environmentObject(gamesViewModel) - .toolbar(.hidden) - .navigationBarHidden(true) - } else { - PastGamesView() - .environmentObject(gamesViewModel) - .toolbar(.hidden) - .navigationBarHidden(true) + switch selectedTab { + case .schedule: + UpcomingGamesView() + .environmentObject(gamesViewModel) + case .highlights: + HighlightView(highlights: Highlight.dummyData) + .environmentObject(gamesViewModel) + case .scores: + PastGamesView() + .environmentObject(gamesViewModel) } HStack { - ForEach(0..<2, id: \.self) { - index in - TabViewIcon(selectionIndex: $selection, itemIndex: index) + ForEach(MainTab.allCases) { tab in + TabViewIcon(selectedTab: $selectedTab, tab: tab) .frame(height: 45) .padding(.top, 10) - if index != 1 { - Spacer() - } + if tab != .scores { Spacer() } } } - .padding(.horizontal, 86) + // Different paddings to balance text lengths + .padding(.trailing, 48) + .padding(.leading, 38) .padding(.bottom, 40) .padding(.top, 8) .frame(maxWidth: .infinity) @@ -59,5 +58,5 @@ struct MainTabView: View { } #Preview { - MainTabView(selection: .constant(0)) + MainTabView(selectedTab: .constant(.schedule)) } diff --git a/score-ios/Views/MainViews/ScoreApp.swift b/score-ios/Views/MainViews/ScoreApp.swift index 170add4..ee975d6 100644 --- a/score-ios/Views/MainViews/ScoreApp.swift +++ b/score-ios/Views/MainViews/ScoreApp.swift @@ -11,13 +11,13 @@ import GameAPI /// Main View of the app struct ContentView: View { - @State private var selectedTab: Int = 0 + @State private var selectedTab: MainTab = .schedule @State private var games: [GamesQuery.Data.Game] = [] @State private var errorMessage: String? var body: some View { - MainTabView(selection: $selectedTab) + MainTabView(selectedTab: $selectedTab) } } @@ -26,9 +26,9 @@ struct ContentView: View { } struct StateWrapper: View { - @State private var selectedTab: Int = 0 + @State private var selectedTab: MainTab = .schedule var body: some View { - MainTabView(selection: $selectedTab) + MainTabView(selectedTab: $selectedTab) } } diff --git a/score-ios/Views/MainViews/SearchViewFullScreen.swift b/score-ios/Views/MainViews/SearchViewFullScreen.swift new file mode 100644 index 0000000..c609746 --- /dev/null +++ b/score-ios/Views/MainViews/SearchViewFullScreen.swift @@ -0,0 +1,156 @@ +// +// SearchViewFullScreen.swift +// score-ios +// +// Created by Zain Bilal on 10/14/25. +// + +import SwiftUI + +struct SearchViewFullScreen: View { + let title: String + let allHighlights: [Highlight] + + @Environment(\.dismiss) private var dismiss + + @State private var searchText = "" + @State private var filteredHighlights: [Highlight] = [] + @State private var debouncedText = "" + @State private var debounceWorkItem: DispatchWorkItem? + @State private var isLoading = false + + @FocusState private var isSearchFieldFocused: Bool + + private let debounceDelay: TimeInterval = 0.8 + + var body: some View { + VStack(spacing: 0) { + // MARK: Header + HStack { + Text(title) + .padding(.top, 12) + .padding(.horizontal, 24) + .font(Constants.Fonts.subheader) + .foregroundStyle(Constants.Colors.black) + + Spacer() + } + + HStack { + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(Constants.Colors.gray_text) + + TextField("Search Highlights", text: $searchText) + .foregroundColor(Constants.Colors.gray_text) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + .onChange(of: searchText) { _, newValue in + debounceSearch(newValue) + } + .focused($isSearchFieldFocused) + + if !searchText.isEmpty { + Button(action: { searchText = "" }) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(Constants.Colors.gray_text) + } + } + } + .padding(8) + .background( + RoundedRectangle(cornerRadius: 30) + .stroke(Constants.Colors.gray_border, lineWidth: 1) + ) + + Button("Cancel") { + dismiss() + } + .foregroundColor(Constants.Colors.gray_text) + .padding(.horizontal, 6) + } + .padding() + .padding(.horizontal, 6) + + // MARK: Results + if searchText.isEmpty { + Spacer() + } else if isLoading { + VStack { + Spacer() + + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .scaleEffect(1.2) + + Spacer() + } + } else if filteredHighlights.isEmpty { + VStack { + NoHighlightView() + } + } else { + ScrollView { + HStack { + Text("\(filteredHighlights.count) results") + .padding(.top, 12) + .padding(.horizontal, 24) + .font(Constants.Fonts.subheader) + .foregroundStyle(Constants.Colors.gray_text) + + Spacer() + } + + LazyVStack(alignment: .leading, spacing: 24) { + ForEach(filteredHighlights) { highlight in + HighlightTile(highlight: highlight, width: 360) + .padding(.horizontal, 24) + } + } + } + .transition(.opacity) + } + } + .onAppear { + filteredHighlights = allHighlights + isSearchFieldFocused = true + } + } + + // MARK: - Debounce + private func debounceSearch(_ text: String) { + debounceWorkItem?.cancel() + isLoading = true + + let workItem = DispatchWorkItem { + DispatchQueue.main.async { + let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) + debouncedText = trimmed + if trimmed.isEmpty { + filteredHighlights = allHighlights + } else { + filteredHighlights = allHighlights.filter { highlight in + highlightTitle(highlight).localizedCaseInsensitiveContains(trimmed) + } + } + + isLoading = false + } + } + + debounceWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + debounceDelay, execute: workItem) + } + + private func highlightTitle(_ highlight: Highlight) -> String { + switch highlight { + case .video(let video): return video.title + case .article(let article): return article.title + } + } +} + +// MARK: - Preview +#Preview { + SearchViewFullScreen(title: "Search All Highlights", allHighlights: Highlight.dummyData) +} diff --git a/score-ios/Views/MainViews/TabViewIcon.swift b/score-ios/Views/MainViews/TabViewIcon.swift index 2937b87..c435e30 100644 --- a/score-ios/Views/MainViews/TabViewIcon.swift +++ b/score-ios/Views/MainViews/TabViewIcon.swift @@ -7,32 +7,44 @@ import SwiftUI -struct TabViewIcon: View { - - // MARK: - Properties - - @Binding var selectionIndex: Int - - let itemIndex: Int - private let tabItems = ["schedule", "scoreboard"] +enum MainTab: String, CaseIterable, Identifiable { + case schedule = "Schedule" + case highlights = "Highlights" + case scores = "Scores" + + var id: Self { self } + + var imageName: String { + switch self { + case .schedule: return "schedule" + case .highlights: return "highlight" + case .scores: return "scoreboard" + } + } +} +struct TabViewIcon: View { + @Binding var selectedTab: MainTab + let tab: MainTab + var body: some View { Button { - selectionIndex = itemIndex + selectedTab = tab } label: { VStack { - Image(itemIndex == selectionIndex ? "\(tabItems[itemIndex])-selected" : tabItems[itemIndex]) + Image(selectedTab == tab ? "\(tab.imageName)-selected" : tab.imageName) .resizable() .frame(width: 28, height: 28) .tint(Constants.Colors.gray_icons) - Text(itemIndex == 0 ? "Schedule" : "Scores") + + Text(tab.rawValue) .font(Constants.Fonts.buttonLabel) - .foregroundStyle(itemIndex == selectionIndex ? Constants.Colors.primary_red : Constants.Colors.unselectedText) + .foregroundStyle(selectedTab == tab ? Constants.Colors.primary_red : Constants.Colors.unselectedText) } } } } #Preview { - TabViewIcon(selectionIndex: .constant(0), itemIndex: 0) + TabViewIcon(selectedTab: .constant(.schedule), tab: .schedule) }