diff --git a/score-ios/Models/GraphQL/schema.graphqls b/score-ios/Models/GraphQL/schema.graphqls index a02c9e1..9a57275 100644 --- a/score-ios/Models/GraphQL/schema.graphqls +++ b/score-ios/Models/GraphQL/schema.graphqls @@ -34,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!, ticketLink: String): GameType + gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!): GameType gamesBySport(sport: String!): [GameType] gamesByGender(gender: String!): [GameType] gamesBySportGender(sport: String!, gender: String!): [GameType] @@ -70,7 +70,7 @@ Attributes: - id: The YouTube video ID (optional). - title: The title of the video. - description: The description of the video. - - thumbnail: The URL of the video's thumbnail. + - thumbnail: The URL of the video's thumbnail. (optional) - url: The URL to the video. - published_at: The date and time the video was published. - duration: The duration of the video (optional). @@ -81,7 +81,7 @@ type YoutubeVideoType { title: String! description: String! thumbnail: String! - b64Thumbnail: String! + b64Thumbnail: String url: String! publishedAt: String! duration: String @@ -183,7 +183,7 @@ type Mutation { createTeam(b64Image: String, color: String!, image: String, name: String!): CreateTeam """Creates a new youtube video.""" - createYoutubeVideo(b64Thumbnail: String!, description: String!, duration: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo + createYoutubeVideo(b64Thumbnail: String, description: String!, duration: 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 @@ -203,4 +203,4 @@ type CreateYoutubeVideo { type CreateArticle { article: ArticleType -} +} \ No newline at end of file diff --git a/score-ios/Models/Sport.swift b/score-ios/Models/Sport.swift index 128c3cc..4bb86d1 100644 --- a/score-ios/Models/Sport.swift +++ b/score-ios/Models/Sport.swift @@ -15,32 +15,16 @@ enum Sport : String, Identifiable, CaseIterable, CustomStringConvertible { // Both case Basketball - // case CrossCountry case IceHockey case Lacrosse case Soccer - // case Squash - // case SwimmingDiving - // case Tennis - // case TrackField // Women - // case Fencing case FieldHockey - // case Gymnastics - // case Rowing - // case Sailing - // case Softball - // case Volleyball // Men case Baseball case Football - // case Golf - // case RowingHeavyweight - // case RowingLightweight - // case SprintFootball - // case Wrestling // init from a string from backend (might include spaces) init?(normalizedValue: String) { @@ -60,28 +44,12 @@ enum Sport : String, Identifiable, CaseIterable, CustomStringConvertible { switch self { case .All: return "All" case .Basketball: return "Basketball" - // case .CrossCountry: return "Cross Country" case .IceHockey: return "Ice Hockey" case .Lacrosse: return "Lacrosse" case .Soccer: return "Soccer" - // case .Squash: return "Squash" - // case .SwimmingDiving: return "Swimming" - // case .Tennis: return "Tennis" - // case .TrackField: return "Track and Field" - // case .Fencing: return "Fencing" case .FieldHockey: return "Field Hockey" - // case .Gymnastics: return "Gymnastics" - // case .Rowing: return "Rowing" - // case .Sailing: return "Sailing" - // case .Softball: return "Softball" - // case .Volleyball: return "Volleyball" case .Baseball: return "Baseball" case .Football: return "Football" - // case .Golf: return "Golf" - // case .RowingHeavyweight: return "HW Rowing" - // case .RowingLightweight: return "LW Rowing" - // case .SprintFootball: return "Sprint Football" - // case .Wrestling: return "Wrestling" } } } diff --git a/score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/highlight-selected.imageset/Contents.json similarity index 100% rename from score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/Contents.json rename to score-ios/Resources/Assets.xcassets/highlight-selected.imageset/Contents.json 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 similarity index 100% rename from score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/highlight-selected.png rename to score-ios/Resources/Assets.xcassets/highlight-selected.imageset/highlight-selected.png 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 similarity index 100% rename from score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/highlight-selectedx2.png rename to score-ios/Resources/Assets.xcassets/highlight-selected.imageset/highlight-selectedx2.png 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 similarity index 100% rename from score-ios/Resources/Assets.xcassets/Highlight-selected.imageset/highlight-selectedx3.png rename to score-ios/Resources/Assets.xcassets/highlight-selected.imageset/highlight-selectedx3.png diff --git a/score-ios/ViewModels/GamesViewModel.swift b/score-ios/ViewModels/GamesViewModel.swift index 910b8e9..e9cc9aa 100644 --- a/score-ios/ViewModels/GamesViewModel.swift +++ b/score-ios/ViewModels/GamesViewModel.swift @@ -12,7 +12,8 @@ import GameAPI // State enum to track the loading state enum DataState { case idle // Initial state, nothing has been fetched yet - case loading // Fetch in progress + case loading // Initial fetch in progress + case refreshing // Refreshing in progress case success // Fetch completed successfully case error(error: ScoreError) // Fetch failed with an error message } @@ -143,11 +144,10 @@ class GamesViewModel: ObservableObject // TODO: Remove once backend is has implemented pagination with sorted dates and pages by game type func fetchGames() { // Set loading state before fetch - dataState = .loading - // Clear the current arrays - self.games.removeAll() - self.allPastGames.removeAll() - self.allUpcomingGames.removeAll() + dataState = (hasNotFetchedYet ? .loading : .refreshing) + + self.privateUpcomingGames.removeAll() + self.privatePastGames.removeAll() // Start fetching from the first page fetchGamesRecursively(limit: 50, offset: 0, accumulatedGames: []) @@ -272,7 +272,7 @@ class GamesViewModel: ObservableObject // Sort all the collections self.allPastGames.sort(by: {$0.date > $1.date}) self.allUpcomingGames.sort(by: {$0.date < $1.date}) - self.games = updatedGames.sorted(by: {$0.date < $1.date}) + self.games.sort(by: {$0.date < $1.date}) self.topUpcomingGames = Array(self.allUpcomingGames.prefix(3)) self.topPastGames = Array(self.allPastGames.prefix(3)) self.filter() diff --git a/score-ios/ViewModels/HighlightsViewModel.swift b/score-ios/ViewModels/HighlightsViewModel.swift index 73a51d3..253156c 100644 --- a/score-ios/ViewModels/HighlightsViewModel.swift +++ b/score-ios/ViewModels/HighlightsViewModel.swift @@ -38,14 +38,7 @@ class HighlightsViewModel: ObservableObject { // MARK: - Loading func loadHighlights() { - dataState = .loading - - self.allHighlights.removeAll() - self.mainTodayHighlights.removeAll() - self.mainPastThreeDaysHighlights.removeAll() - self.detailedTodayHighlights.removeAll() - self.detailedPastThreeDaysHighlights.removeAll() - self.allHighlightsSearchResults.removeAll() + dataState = (hasNotFetchedYet ? .loading : .refreshing) Task { do { @@ -61,7 +54,7 @@ class HighlightsViewModel: ObservableObject { } } - func retryFetch() { + func retryFetch(isRefresh: Bool) { loadHighlights() } @@ -71,8 +64,8 @@ class HighlightsViewModel: ObservableObject { private func processHighlights(_ articleDataArray: [ArticlesQuery.Data.Article], _ youTubeVideoDataArray: [YoutubeVideosQuery.Data.YoutubeVideo]) { let localArticles = articleDataArray.map { Article(from: $0) } let localYouTubeVideos = youTubeVideoDataArray.map {YouTubeVideo(from: $0)} - - self.privateAllHighlights += localArticles.map { Highlight.article($0) } + localYouTubeVideos.map { Highlight.video($0) } + + self.privateAllHighlights = localArticles.map { Highlight.article($0) } + localYouTubeVideos.map { Highlight.video($0) } self.allHighlights = self.uniqueHighlights(from: self.privateAllHighlights) self.allHighlights.sort(by: { $0.publishedAt > $1.publishedAt }) self.filter() diff --git a/score-ios/Views/DetailedViews/DetailedHighlightView.swift b/score-ios/Views/DetailedViews/DetailedHighlightView.swift index 7c4511f..25902dd 100644 --- a/score-ios/Views/DetailedViews/DetailedHighlightView.swift +++ b/score-ios/Views/DetailedViews/DetailedHighlightView.swift @@ -15,45 +15,18 @@ struct DetailedHighlightsView: View { var highlightScope: HighlightsScope var body: some View { - VStack(alignment: .leading, spacing: 16) { - // Custom header - 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) + VStack{ + headerView - Divider().background(.clear) - - VStack(alignment: .leading, spacing: 0) { - SearchView(title: "Search \(title)", scope: highlightScope) - .padding(.horizontal, 24) - .padding(.top, 20) - - SportSelectorView() - .padding(.top, 20) - } - .cornerRadius(12, corners: [.bottomLeft, .bottomRight]) - - VStack(alignment: .leading, spacing: 0) { - if(highlightsForScope.isEmpty) { - NoHighlightView() - .frame(maxWidth: .infinity) - } - else{ - ScrollView{ + ScrollView { + LazyVStack(alignment: .leading, spacing: 16) { + if(highlightsForScope.isEmpty) { + NoHighlightView() + .frame(maxWidth: .infinity) + .frame(minHeight: UIScreen.main.bounds.height - 350) + // push view to the middle of the screen + } + else{ LazyVStack(alignment: .center) { ForEach(highlightsForScope, id: \.id) { highlight in HighlightTile(highlight: highlight, isVertical: true) @@ -64,14 +37,17 @@ struct DetailedHighlightsView: View { } } } + .background(Constants.Colors.white.ignoresSafeArea()) + .refreshable { + viewModel.loadHighlights() + } + .safeAreaInset(edge: .bottom) { + Color.clear.frame(height: 200) + } + + .navigationBarBackButtonHidden(true) + .navigationBarTitleDisplayMode(.inline) } - .safeAreaInset(edge: .bottom) { - Color.clear.frame(height: 200) - } - - .navigationBarBackButtonHidden(true) - .navigationBarTitleDisplayMode(.inline) - .environmentObject(viewModel) .onAppear { if viewModel.hasNotFetchedYet { @@ -96,12 +72,50 @@ struct DetailedHighlightsView: View { return viewModel.allHighlights } } + + private var headerView: some View { + VStack(alignment: .leading, spacing: 16) { + // Custom header + 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(title: "Search \(title)", scope: highlightScope) + .padding(.horizontal, 24) + .padding(.top, 20) + + SportSelectorView() + .padding(.top, 20) + } + .cornerRadius(12, corners: [.bottomLeft, .bottomRight]) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 12) + .background(Constants.Colors.white) + } } #Preview { DetailedHighlightsView( title: "Today", - highlightScope: .pastThreeDays + highlightScope: .today ) .environmentObject(HighlightsViewModel.shared) } diff --git a/score-ios/Views/ListViews/HighlightView.swift b/score-ios/Views/ListViews/HighlightView.swift index 892fa82..6299c86 100644 --- a/score-ios/Views/ListViews/HighlightView.swift +++ b/score-ios/Views/ListViews/HighlightView.swift @@ -11,12 +11,18 @@ struct HighlightView: View { @EnvironmentObject var viewModel: HighlightsViewModel var body: some View { + Group{ switch viewModel.dataState { case .idle, .loading: HighlightLoadingView() + default: - HighlightContentView() + VStack{ + headerView + + HighlightContentView() + } } } .onAppear { @@ -29,30 +35,36 @@ struct HighlightView: View { viewModel.filter() } } + + var headerView: some View { + VStack { + Text("Highlights") + .font(Constants.Fonts.semibold24) + .foregroundStyle(Constants.Colors.black) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 24) + .padding(.horizontal, 24) + + SearchView(title: "Search All Highlights", scope: .all) + .padding(.horizontal, 20) + .padding(.top, 12) + + SportSelectorView() + .padding(.horizontal, 20) + .padding(.top, 12) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 12) + .background(Constants.Colors.white) + } } struct HighlightContentView: View { @EnvironmentObject var viewModel: HighlightsViewModel var body: some View { - ScrollView(showsIndicators: false) { - 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(title: "Search All Highlights", scope: .all) - .padding(.horizontal, 20) - .padding(.top, 12) - - SportSelectorView() - .padding(.horizontal, 20) - .padding(.top, 12) - + LazyVStack(alignment: .leading, spacing: 4) { if viewModel.mainPastThreeDaysHighlights.isEmpty && viewModel.mainTodayHighlights.isEmpty { NoHighlightView() .frame(maxWidth: .infinity) @@ -76,6 +88,9 @@ struct HighlightContentView: View { } } } + .refreshable { + viewModel.loadHighlights() + } } } diff --git a/score-ios/Views/ListViews/PastGamesView.swift b/score-ios/Views/ListViews/PastGamesView.swift index 0bc7d03..6e855c1 100644 --- a/score-ios/Views/ListViews/PastGamesView.swift +++ b/score-ios/Views/ListViews/PastGamesView.swift @@ -53,6 +53,9 @@ struct PastGamesView: View { vm.fetchGames() } } + .refreshable { + vm.fetchGames() + } .onChange(of: vm.selectedSport) { vm.filter() } diff --git a/score-ios/Views/ListViews/SportSelectorView.swift b/score-ios/Views/ListViews/SportSelectorView.swift index 15a9cda..afcdded 100644 --- a/score-ios/Views/ListViews/SportSelectorView.swift +++ b/score-ios/Views/ListViews/SportSelectorView.swift @@ -8,7 +8,8 @@ import SwiftUI struct SportSelectorView: View { - @ObservedObject private var vm = HighlightsViewModel.shared + @ObservedObject private var highlightsVM = HighlightsViewModel.shared + @ObservedObject private var gamesVM = GamesViewModel.shared @State private var scrollOffset: CGFloat = 0 var body: some View { @@ -17,12 +18,13 @@ struct SportSelectorView: View { HStack { ForEach(Sport.allCases) { sport in Button { - vm.selectedSport = sport + highlightsVM.selectedSport = sport + gamesVM.selectedSport = sport withAnimation { proxy.scrollTo(sport.id, anchor: .center) } } label: { - FilterTile(sport: sport, selected: sport == vm.selectedSport) + FilterTile(sport: sport, selected: sport == highlightsVM.selectedSport) } .id(sport.id) } @@ -32,12 +34,12 @@ struct SportSelectorView: View { .preference(key: ScrollOffsetKey.self, value: geometry.frame(in: .global).minX) }) .onPreferenceChange(ScrollOffsetKey.self) { value in - vm.sportSelectorOffset = value // Save scroll position in ViewModel + highlightsVM.sportSelectorOffset = value // Save scroll position in ViewModel } } .onAppear { DispatchQueue.main.async { - proxy.scrollTo(vm.selectedSport.id, anchor: .center) + proxy.scrollTo(highlightsVM.selectedSport.id, anchor: .center) } } } diff --git a/score-ios/Views/ListViews/UpcomingGamesView.swift b/score-ios/Views/ListViews/UpcomingGamesView.swift index 01f3ea9..b6061ac 100644 --- a/score-ios/Views/ListViews/UpcomingGamesView.swift +++ b/score-ios/Views/ListViews/UpcomingGamesView.swift @@ -56,6 +56,9 @@ struct UpcomingGamesView: View { vm.fetchGames() } } + .refreshable { + vm.fetchGames() + } .onChange(of: vm.selectedSport) { vm.filter() } diff --git a/score-ios/Views/MainViews/SearchViewFullScreen.swift b/score-ios/Views/MainViews/SearchViewFullScreen.swift index 5e7163d..1a73a73 100644 --- a/score-ios/Views/MainViews/SearchViewFullScreen.swift +++ b/score-ios/Views/MainViews/SearchViewFullScreen.swift @@ -37,6 +37,56 @@ struct SearchViewFullScreen: View { var body: some View { + VStack{ + headerView + + ScrollView { + LazyVStack(alignment: .leading, spacing: 0) { + // MARK: Results + if isLoading { + HighlightSearchLoadingView() + } else if searchResults.isEmpty { + NoHighlightView() + .frame(maxWidth: .infinity) + .frame(minHeight: UIScreen.main.bounds.height - 350) + // push view to the middle of the screen + } else { + Text("\(searchResults.count) results") + .padding(.horizontal, 20) + .font(Constants.Fonts.subheader) + .foregroundStyle(Constants.Colors.gray_text) + .frame(maxWidth: .infinity, alignment: .leading) + + + + LazyVStack(alignment: .center, spacing: 24) { + ForEach(searchResults) { highlight in + HighlightTile(highlight: highlight, isVertical: true) + .padding(.horizontal, 24) + } + } + .padding(.top, 12) + } + } + } + .refreshable { + viewModel.loadHighlights() + } + } + .onAppear { + if viewModel.hasNotFetchedYet{ + viewModel.loadHighlights() + } + isSearchFieldFocused = true + searchText = viewModel.searchQuery + viewModel.filter() + } + .onDisappear { + viewModel.clearSearch() + } + } + + private var headerView: some View { VStack(spacing: 0) { // MARK: Header HStack { @@ -91,46 +141,11 @@ struct SearchViewFullScreen: View { SportSelectorView() .padding(.horizontal, 20) .padding(.top, 12) - .padding(.bottom, 20) .cornerRadius(12, corners: [.bottomLeft, .bottomRight]) - - // MARK: Results - VStack(alignment: .leading, spacing: 0) - { - if isLoading { - HighlightSearchLoadingView() - } else if searchResults.isEmpty { - NoHighlightView() - } else { - ScrollView { - HStack { - Text("\(searchResults.count) results") - .padding(.horizontal, 24) - .font(Constants.Fonts.subheader) - .foregroundStyle(Constants.Colors.gray_text) - - Spacer() - } - - LazyVStack(alignment: .center, spacing: 24) { - ForEach(searchResults) { highlight in - HighlightTile(highlight: highlight, isVertical: true) - .padding(.horizontal, 24) - } - } - } - .transition(.opacity) - } - } - } - .onAppear { - isSearchFieldFocused = true - searchText = viewModel.searchQuery - viewModel.filter() - } - .onDisappear { - viewModel.clearSearch() } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 12) + .background(Constants.Colors.white) }