From 296242d54a4d04cf7614a4567aec877ac124c46d Mon Sep 17 00:00:00 2001 From: Zain Bilal Date: Mon, 27 Oct 2025 16:05:12 -0400 Subject: [PATCH 01/12] refactored highlight logic into HighlightViewModel --- score-ios.xcodeproj/project.pbxproj | 4 + score-ios/Models/Highlight.swift | 12 +- .../ViewModels/HighlightsViewModel.swift | 284 ++++++++++++++++++ .../DetailedViews/DetailedHighlightView.swift | 3 +- score-ios/Views/ListViews/HighlightView.swift | 33 +- score-ios/Views/ListViews/SearchView.swift | 7 +- score-ios/Views/MainViews/MainTabView.swift | 2 +- .../MainViews/SearchViewFullScreen.swift | 41 +-- 8 files changed, 326 insertions(+), 60 deletions(-) create mode 100644 score-ios/ViewModels/HighlightsViewModel.swift diff --git a/score-ios.xcodeproj/project.pbxproj b/score-ios.xcodeproj/project.pbxproj index fb89448..fe2830b 100644 --- a/score-ios.xcodeproj/project.pbxproj +++ b/score-ios.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 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 */; }; + 7665A4072EB00531004A9903 /* HighlightsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7665A4062EB00528004A9903 /* HighlightsViewModel.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 */; }; @@ -137,6 +138,7 @@ 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 = ""; }; + 7665A4062EB00528004A9903 /* HighlightsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightsViewModel.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 = ""; }; @@ -507,6 +509,7 @@ isa = PBXGroup; children = ( D87787C72CFFAE5200EA79E1 /* GamesViewModel.swift */, + 7665A4062EB00528004A9903 /* HighlightsViewModel.swift */, D864B5AA2D793A7400A3A50E /* PastGameViewModel.swift */, ); path = ViewModels; @@ -770,6 +773,7 @@ CE528FA42C9653C200C238B5 /* Error.swift in Sources */, CE335CD52C922ECB0037F572 /* Constants.swift in Sources */, CE8ED4FE2D6BF49E00A274DE /* GameUpdate.swift in Sources */, + 7665A4072EB00531004A9903 /* HighlightsViewModel.swift in Sources */, D86347E12CE98D37003DD8F6 /* TabViewIcon.swift in Sources */, CE3C9C412D010177008BFB4C /* ScoringSummary.swift in Sources */, CE8ED50E2D6C3B8000A274DE /* SportSelectorView.swift in Sources */, diff --git a/score-ios/Models/Highlight.swift b/score-ios/Models/Highlight.swift index 586309f..17c6367 100644 --- a/score-ios/Models/Highlight.swift +++ b/score-ios/Models/Highlight.swift @@ -50,7 +50,7 @@ extension Highlight { thumbnail: "https://i.ytimg.com/vi/QGHb9heJAco/hqdefault.jpg", b64Thumbnail: nil, url: "https://youtube.com/watch?v=QGHb9heJAco", - publishedAt: "2025-10-014T00:00:00Z" + publishedAt: "2025-10-0227T00:00:00Z" ) ), .article( @@ -61,7 +61,7 @@ extension Highlight { 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" + publishedAt: "2025-10-27T00:00:00Z" ) ), .video( @@ -72,7 +72,7 @@ extension Highlight { thumbnail: "https://i.ytimg.com/vi/ABC123def/hqdefault.jpg", b64Thumbnail: nil, url: "https://youtube.com/watch?v=ABC123def", - publishedAt: "2025-10-013T00:00:00Z" + publishedAt: "2025-10-027T00:00:00Z" ) ), .article( @@ -83,7 +83,7 @@ extension Highlight { 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" + publishedAt: "2025-10-26T00:00:00Z" ) ), .video( @@ -94,7 +94,7 @@ extension Highlight { thumbnail: "https://i.ytimg.com/vi/XYZ789ghi/hqdefault.jpg", b64Thumbnail: nil, url: "https://youtube.com/watch?v=XYZ789ghi", - publishedAt: "2025-10-14T00:00:00Z" + publishedAt: "2025-10-26T00:00:00Z" ) ), .article( @@ -105,7 +105,7 @@ extension Highlight { 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" + publishedAt: "2025-10-27T00:00:00Z" ) ) ] diff --git a/score-ios/ViewModels/HighlightsViewModel.swift b/score-ios/ViewModels/HighlightsViewModel.swift new file mode 100644 index 0000000..d809954 --- /dev/null +++ b/score-ios/ViewModels/HighlightsViewModel.swift @@ -0,0 +1,284 @@ +// +// HighlightsViewModel.swift +// score-ios +// +// Created by Zain Bilal on 10/27/25. +// + +import Foundation +import SwiftUI + +class HighlightsViewModel: ObservableObject { + // MARK: - Published Properties + + /// All highlights (videos and articles) + @Published var highlights: [Highlight] = [] + + /// Highlights published today + @Published var todayHighlights: [Highlight] = [] + + /// Highlights published in the past 3 days + @Published var pastThreeDaysHighlights: [Highlight] = [] + + /// Filtered highlights based on search query + @Published var filteredHighlights: [Highlight] = [] + + /// Current search query + @Published var searchQuery: String = "" + + /// Loading state + @Published var isLoading: Bool = false + + /// Current filter state + @Published var dataState: DataState = .idle + + /// Selected sport filter (if any) + @Published var selectedSport: Sport? + + // MARK: - Private Properties + + private var allHighlights: [Highlight] = [] + + // MARK: - Singleton + + static let shared = HighlightsViewModel() + + // MARK: - Computed Properties + + /// Returns whether there are any highlights + var hasHighlights: Bool { + return !highlights.isEmpty + } + + /// Returns whether there are today's highlights + var hasTodayHighlights: Bool { + return !todayHighlights.isEmpty + } + + /// Returns whether there are past 3 days highlights + var hasPastThreeDaysHighlights: Bool { + return !pastThreeDaysHighlights.isEmpty + } + + /// Returns the count of filtered results + var filteredCount: Int { + return filteredHighlights.count + } + + /// Returns whether data has been fetched yet + var hasNotFetchedYet: Bool { + return dataState == .idle + } + + // MARK: - Initialization + + private init(highlights: [Highlight] = []) { + self.allHighlights = highlights + self.highlights = highlights + self.filteredHighlights = highlights + setupData() + } + + // MARK: - Setup Methods + + /// Setup initial data (filter by dates) + private func setupData() { + todayHighlights = getTodayHighlights() + pastThreeDaysHighlights = getPastThreeDaysHighlights() + } + + /// Get highlights published today + private func getTodayHighlights() -> [Highlight] { + return highlights.filter { + if let date = Date.fullDateFormatter.date(from: $0.publishedAt) { + return Date.isWithinPastDays(date, days: 1) + } + return false + } + } + + /// Get highlights from past 3 days (excluding today) + private func getPastThreeDaysHighlights() -> [Highlight] { + return 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) + } + } + + // MARK: - Data Management + + /// Load highlights from dummy data or external source + func loadHighlights() { + isLoading = true + dataState = .loading + + // For now, use dummy data + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + guard let self = self else { return } + + self.allHighlights = Highlight.dummyData + self.highlights = Highlight.dummyData + self.filteredHighlights = Highlight.dummyData + self.setupData() + + self.isLoading = false + self.dataState = .success + } + } + + /// Update highlights data + func updateHighlights(_ newHighlights: [Highlight]) { + allHighlights = newHighlights + highlights = newHighlights + setupData() + applyFilters() + } + + /// Refresh highlights + func refreshHighlights() { + loadHighlights() + } + + // MARK: - Search Functionality + + /// Filter highlights by search query + func filterBySearch(_ query: String) { + searchQuery = query + applyFilters() + } + + /// Clear search + func clearSearch() { + searchQuery = "" + applyFilters() + } + + /// Apply all active filters (search and sport) + private func applyFilters() { + var filtered = allHighlights + + // Apply search filter + if !searchQuery.isEmpty { + let trimmedQuery = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines) + filtered = filtered.filter { highlight in + highlightTitle(highlight).localizedCaseInsensitiveContains(trimmedQuery) + } + } + + // Apply sport filter (if needed in future) + if let sport = selectedSport { + // This would need to be implemented based on how highlights relate to sports + // filtered = filtered.filter { /* sport logic */ } + } + + filteredHighlights = filtered + } + + /// Get title from highlight + private func highlightTitle(_ highlight: Highlight) -> String { + switch highlight { + case .video(let video): + return video.title + case .article(let article): + return article.title + } + } + + // MARK: - Filter by Content Type + + /// Get all video highlights + func getVideos() -> [YouTubeVideo] { + return highlights.compactMap { highlight in + if case .video(let video) = highlight { + return video + } + return nil + } + } + + /// Get all article highlights + func getArticles() -> [Article] { + return highlights.compactMap { highlight in + if case .article(let article) = highlight { + return article + } + return nil + } + } + + /// Filter highlights by content type + func getHighlightsByType(isVideo: Bool) -> [Highlight] { + if isVideo { + return highlights.filter { + if case .video = $0 { + return true + } + return false + } + } else { + return highlights.filter { + if case .article = $0 { + return true + } + return false + } + } + } + + // MARK: - Error Handling + + /// Handle error state + func handleError(_ error: ScoreError) { + DispatchQueue.main.async { [weak self] in + self?.dataState = .error(error: error) + self?.isLoading = false + } + } + + /// Retry after error + func retry() { + loadHighlights() + } + + // MARK: - Helper Methods + + /// Get highlight by ID + func getHighlight(by id: String) -> Highlight? { + return highlights.first { $0.id == id } + } + + /// Check if highlight is a video + func isVideo(_ highlight: Highlight) -> Bool { + if case .video = highlight { + return true + } + return false + } + + /// Check if highlight is an article + func isArticle(_ highlight: Highlight) -> Bool { + if case .article = highlight { + return true + } + return false + } +} + +// MARK: - DataState Extension +extension DataState { + /// Check if state is in error state + var isError: Bool { + if case .error = self { + return true + } + return false + } + + /// Get error if in error state + var error: ScoreError? { + if case .error(let error) = self { + return error + } + return nil + } +} diff --git a/score-ios/Views/DetailedViews/DetailedHighlightView.swift b/score-ios/Views/DetailedViews/DetailedHighlightView.swift index 3f9ae06..a5c8ae8 100644 --- a/score-ios/Views/DetailedViews/DetailedHighlightView.swift +++ b/score-ios/Views/DetailedViews/DetailedHighlightView.swift @@ -38,7 +38,7 @@ struct DetailedHighlightsView: View { .background(.clear) VStack(alignment: .leading, spacing: 0) { - SearchView(highlights: highlights, title: "Search \(title)") + SearchView(title: "Search \(title)") .padding(.horizontal, 24) .padding(.top, 20) @@ -72,4 +72,5 @@ struct DetailedHighlightsView: View { title: "Today", highlights: Highlight.dummyData ) + .environmentObject(HighlightsViewModel.shared) } diff --git a/score-ios/Views/ListViews/HighlightView.swift b/score-ios/Views/ListViews/HighlightView.swift index 0ff0810..1002acb 100644 --- a/score-ios/Views/ListViews/HighlightView.swift +++ b/score-ios/Views/ListViews/HighlightView.swift @@ -8,22 +8,9 @@ import SwiftUI struct HighlightView: View { - @State var highlights: [Highlight] + @ObservedObject private var viewModel = HighlightsViewModel.shared 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) { @@ -34,7 +21,7 @@ struct HighlightView: View { .padding(.top, 24) .padding(.horizontal, 24) - SearchView(highlights: highlights, title: "Search All Highlights") + SearchView(title: "Search All Highlights") .padding(.horizontal, 20) .padding(.top, 12) @@ -42,16 +29,22 @@ struct HighlightView: View { .padding(.horizontal, 20) .padding(.top, 12) - if !todayHighlights.isEmpty { - HighlightSectionView(title: "Today", highlights: todayHighlights) + if viewModel.hasTodayHighlights { + HighlightSectionView(title: "Today", highlights: viewModel.todayHighlights) } - if !pastThreeDaysHighlights.isEmpty { - HighlightSectionView(title: "Past 3 Days", highlights: pastThreeDaysHighlights) + if viewModel.hasPastThreeDaysHighlights { + HighlightSectionView(title: "Past 3 Days", highlights: viewModel.pastThreeDaysHighlights) } } } } + .environmentObject(viewModel) + .onAppear { + if viewModel.hasNotFetchedYet { + viewModel.loadHighlights() + } + } } } @@ -99,5 +92,5 @@ struct HighlightSectionView: View { // MARK: - Preview #Preview { - HighlightView(highlights: Highlight.dummyData) + HighlightView() } diff --git a/score-ios/Views/ListViews/SearchView.swift b/score-ios/Views/ListViews/SearchView.swift index afd1734..7fb5c61 100644 --- a/score-ios/Views/ListViews/SearchView.swift +++ b/score-ios/Views/ListViews/SearchView.swift @@ -8,8 +8,8 @@ import SwiftUI struct SearchView: View { + @ObservedObject private var viewModel = HighlightsViewModel.shared @State private var showSearch = false - var highlights: [Highlight] let title: String var body: some View { @@ -31,12 +31,13 @@ struct SearchView: View { } .buttonStyle(PlainButtonStyle()) .fullScreenCover(isPresented: $showSearch) { - SearchViewFullScreen(title: title, allHighlights: highlights) + SearchViewFullScreen(title: title) + .environmentObject(viewModel) } } } #Preview { - SearchView(highlights: Highlight.dummyData, title: "Search All Highlights") + SearchView(title: "Search All Highlights") } diff --git a/score-ios/Views/MainViews/MainTabView.swift b/score-ios/Views/MainViews/MainTabView.swift index 99d9235..1b0f5a7 100644 --- a/score-ios/Views/MainViews/MainTabView.swift +++ b/score-ios/Views/MainViews/MainTabView.swift @@ -22,7 +22,7 @@ struct MainTabView: View { UpcomingGamesView() .environmentObject(gamesViewModel) case .highlights: - HighlightView(highlights: Highlight.dummyData) + HighlightView() .environmentObject(gamesViewModel) case .scores: PastGamesView() diff --git a/score-ios/Views/MainViews/SearchViewFullScreen.swift b/score-ios/Views/MainViews/SearchViewFullScreen.swift index c609746..d8d8637 100644 --- a/score-ios/Views/MainViews/SearchViewFullScreen.swift +++ b/score-ios/Views/MainViews/SearchViewFullScreen.swift @@ -8,16 +8,13 @@ import SwiftUI struct SearchViewFullScreen: View { + @EnvironmentObject private var viewModel: HighlightsViewModel 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 @@ -51,7 +48,10 @@ struct SearchViewFullScreen: View { .focused($isSearchFieldFocused) if !searchText.isEmpty { - Button(action: { searchText = "" }) { + Button(action: { + searchText = "" + viewModel.clearSearch() + }) { Image(systemName: "xmark.circle.fill") .foregroundColor(Constants.Colors.gray_text) } @@ -75,7 +75,7 @@ struct SearchViewFullScreen: View { // MARK: Results if searchText.isEmpty { Spacer() - } else if isLoading { + } else if viewModel.isLoading { VStack { Spacer() @@ -85,14 +85,14 @@ struct SearchViewFullScreen: View { Spacer() } - } else if filteredHighlights.isEmpty { + } else if viewModel.filteredHighlights.isEmpty { VStack { NoHighlightView() } } else { ScrollView { HStack { - Text("\(filteredHighlights.count) results") + Text("\(viewModel.filteredCount) results") .padding(.top, 12) .padding(.horizontal, 24) .font(Constants.Fonts.subheader) @@ -102,7 +102,7 @@ struct SearchViewFullScreen: View { } LazyVStack(alignment: .leading, spacing: 24) { - ForEach(filteredHighlights) { highlight in + ForEach(viewModel.filteredHighlights) { highlight in HighlightTile(highlight: highlight, width: 360) .padding(.horizontal, 24) } @@ -112,7 +112,6 @@ struct SearchViewFullScreen: View { } } .onAppear { - filteredHighlights = allHighlights isSearchFieldFocused = true } } @@ -120,37 +119,21 @@ struct SearchViewFullScreen: View { // 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 + viewModel.filterBySearch(trimmed) } } 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) + SearchViewFullScreen(title: "Search All Highlights") + .environmentObject(HighlightsViewModel.shared) } From 4f1fc2e105e822664a5952e08b5065291ec8492b Mon Sep 17 00:00:00 2001 From: Zain Bilal Date: Wed, 5 Nov 2025 15:34:39 -0500 Subject: [PATCH 02/12] refactored HighlightViewModel --- score-ios/Models/Article.swift | 43 +-- score-ios/Models/Highlight.swift | 27 +- score-ios/Models/YouTubeVideo.swift | 43 +-- .../ViewModels/HighlightsViewModel.swift | 325 +++++------------- .../DetailedViews/DetailedHighlightView.swift | 70 ++-- .../ListViews/HighlightTileArticle.swift | 8 - .../Views/ListViews/HighlightTileVideo.swift | 5 - score-ios/Views/ListViews/HighlightView.swift | 57 ++- score-ios/Views/ListViews/SearchView.swift | 8 +- .../Views/ListViews/SportSelectorView.swift | 2 +- .../MainViews/SearchViewFullScreen.swift | 45 ++- 11 files changed, 239 insertions(+), 394 deletions(-) diff --git a/score-ios/Models/Article.swift b/score-ios/Models/Article.swift index b5387c0..45dee41 100644 --- a/score-ios/Models/Article.swift +++ b/score-ios/Models/Article.swift @@ -15,6 +15,7 @@ struct Article: Identifiable { var url: String var source: String var publishedAt: String + var sport: Sport var formattedDate: String { if let date = ISO8601DateFormatter().date(from: publishedAt) { @@ -25,45 +26,3 @@ struct Article: Identifiable { 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/Highlight.swift b/score-ios/Models/Highlight.swift index 17c6367..5c6638f 100644 --- a/score-ios/Models/Highlight.swift +++ b/score-ios/Models/Highlight.swift @@ -37,6 +37,15 @@ enum Highlight: Identifiable { return video.title } } + + var sport: Sport{ + switch self{ + case .article(let article): + return article.sport + case .video(let video): + return video.sport + } + } } // MARK: - Dummy Data @@ -50,7 +59,8 @@ extension Highlight { thumbnail: "https://i.ytimg.com/vi/QGHb9heJAco/hqdefault.jpg", b64Thumbnail: nil, url: "https://youtube.com/watch?v=QGHb9heJAco", - publishedAt: "2025-10-0227T00:00:00Z" + publishedAt: "2025-11-05T00:00:00Z", + sport: .All ) ), .article( @@ -61,7 +71,8 @@ extension Highlight { 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-27T00:00:00Z" + publishedAt: "2025-11-05T00:00:00Z", + sport: .FieldHockey ) ), .video( @@ -72,7 +83,8 @@ extension Highlight { thumbnail: "https://i.ytimg.com/vi/ABC123def/hqdefault.jpg", b64Thumbnail: nil, url: "https://youtube.com/watch?v=ABC123def", - publishedAt: "2025-10-027T00:00:00Z" + publishedAt: "2025-11-05T00:00:00Z", + sport: .Baseball ) ), .article( @@ -83,7 +95,8 @@ extension Highlight { 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-26T00:00:00Z" + publishedAt: "2025-11-05T00:00:00Z", + sport: .Football ) ), .video( @@ -94,7 +107,8 @@ extension Highlight { thumbnail: "https://i.ytimg.com/vi/XYZ789ghi/hqdefault.jpg", b64Thumbnail: nil, url: "https://youtube.com/watch?v=XYZ789ghi", - publishedAt: "2025-10-26T00:00:00Z" + publishedAt: "2025-11-04T00:00:00Z", + sport: .FieldHockey ) ), .article( @@ -105,7 +119,8 @@ extension Highlight { 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-27T00:00:00Z" + publishedAt: "2025-11-04T00:00:00Z", + sport: .Baseball ) ) ] diff --git a/score-ios/Models/YouTubeVideo.swift b/score-ios/Models/YouTubeVideo.swift index e855d28..15dd934 100644 --- a/score-ios/Models/YouTubeVideo.swift +++ b/score-ios/Models/YouTubeVideo.swift @@ -15,6 +15,7 @@ struct YouTubeVideo: Identifiable { var b64Thumbnail: String? var url: String var publishedAt: String + var sport: Sport // Format publishedAt -> MM/dd or similar var formattedDate: String { @@ -26,45 +27,3 @@ struct YouTubeVideo: Identifiable { 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/ViewModels/HighlightsViewModel.swift b/score-ios/ViewModels/HighlightsViewModel.swift index d809954..6abfab0 100644 --- a/score-ios/ViewModels/HighlightsViewModel.swift +++ b/score-ios/ViewModels/HighlightsViewModel.swift @@ -10,275 +10,138 @@ import SwiftUI class HighlightsViewModel: ObservableObject { // MARK: - Published Properties - - /// All highlights (videos and articles) - @Published var highlights: [Highlight] = [] - - /// Highlights published today - @Published var todayHighlights: [Highlight] = [] - - /// Highlights published in the past 3 days - @Published var pastThreeDaysHighlights: [Highlight] = [] - - /// Filtered highlights based on search query - @Published var filteredHighlights: [Highlight] = [] - - /// Current search query - @Published var searchQuery: String = "" - - /// Loading state - @Published var isLoading: Bool = false - - /// Current filter state @Published var dataState: DataState = .idle + @Published var isLoading: Bool = false - /// Selected sport filter (if any) - @Published var selectedSport: Sport? - - // MARK: - Private Properties - - private var allHighlights: [Highlight] = [] + @Published var allHighlights: [Highlight] = [] + @Published var mainTodayHighlights: [Highlight] = [] + @Published var mainPastThreeDaysHighlights: [Highlight] = [] + @Published var detailedTodayHighlights: [Highlight] = [] + @Published var detailedPastThreeDaysHighlights: [Highlight] = [] + @Published var allHighlightsSearchResults: [Highlight] = [] + @Published var searchQuery: String = "" + @Published var selectedSport: Sport = .All + @Published var sportSelectorOffset: CGFloat = 0 + @Published var currentScope: HighlightsScope = .main + // MARK: - Singleton - static let shared = HighlightsViewModel() - - // MARK: - Computed Properties - - /// Returns whether there are any highlights - var hasHighlights: Bool { - return !highlights.isEmpty - } - - /// Returns whether there are today's highlights - var hasTodayHighlights: Bool { - return !todayHighlights.isEmpty - } - - /// Returns whether there are past 3 days highlights - var hasPastThreeDaysHighlights: Bool { - return !pastThreeDaysHighlights.isEmpty - } - - /// Returns the count of filtered results - var filteredCount: Int { - return filteredHighlights.count - } - - /// Returns whether data has been fetched yet - var hasNotFetchedYet: Bool { - return dataState == .idle - } - - // MARK: - Initialization - - private init(highlights: [Highlight] = []) { - self.allHighlights = highlights - self.highlights = highlights - self.filteredHighlights = highlights - setupData() - } - - // MARK: - Setup Methods - - /// Setup initial data (filter by dates) - private func setupData() { - todayHighlights = getTodayHighlights() - pastThreeDaysHighlights = getPastThreeDaysHighlights() - } - - /// Get highlights published today - private func getTodayHighlights() -> [Highlight] { - return highlights.filter { - if let date = Date.fullDateFormatter.date(from: $0.publishedAt) { - return Date.isWithinPastDays(date, days: 1) - } - return false - } - } - - /// Get highlights from past 3 days (excluding today) - private func getPastThreeDaysHighlights() -> [Highlight] { - return 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) - } - } - - // MARK: - Data Management - - /// Load highlights from dummy data or external source + private init() {} + + // MARK: - Computed + var hasNotFetchedYet: Bool { dataState == .idle } + + // MARK: - Loading func loadHighlights() { - isLoading = true dataState = .loading + isLoading = true - // For now, use dummy data - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - guard let self = self else { return } - + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.allHighlights = Highlight.dummyData - self.highlights = Highlight.dummyData - self.filteredHighlights = Highlight.dummyData - self.setupData() - - self.isLoading = false + self.filter() self.dataState = .success + self.isLoading = false } } - /// Update highlights data - func updateHighlights(_ newHighlights: [Highlight]) { - allHighlights = newHighlights - highlights = newHighlights - setupData() - applyFilters() - } - - /// Refresh highlights func refreshHighlights() { loadHighlights() } - - // MARK: - Search Functionality - - /// Filter highlights by search query - func filterBySearch(_ query: String) { - searchQuery = query - applyFilters() - } - - /// Clear search - func clearSearch() { - searchQuery = "" - applyFilters() - } - - /// Apply all active filters (search and sport) - private func applyFilters() { - var filtered = allHighlights - - // Apply search filter - if !searchQuery.isEmpty { - let trimmedQuery = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines) - filtered = filtered.filter { highlight in - highlightTitle(highlight).localizedCaseInsensitiveContains(trimmedQuery) - } + + // MARK: - Filtering + func filter() { + let filteredBySport: [Highlight] + if selectedSport == .All { + filteredBySport = allHighlights + } else { + filteredBySport = allHighlights.filter { $0.sport == selectedSport } } - - // Apply sport filter (if needed in future) - if let sport = selectedSport { - // This would need to be implemented based on how highlights relate to sports - // filtered = filtered.filter { /* sport logic */ } + + let filteredBySearch: [Highlight] + if searchQuery.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + filteredBySearch = filteredBySport + } else { + let query = searchQuery.lowercased() + filteredBySearch = filteredBySport.filter { + highlightTitle($0).lowercased().contains(query) + } } - - filteredHighlights = filtered - } - - /// Get title from highlight - private func highlightTitle(_ highlight: Highlight) -> String { - switch highlight { - case .video(let video): - return video.title - case .article(let article): - return article.title + + // --- Main Page Filters (by sport only) + mainTodayHighlights = filteredBySport.filter { + guard let date = Date.fullDateFormatter.date(from: $0.publishedAt) else { return false } + return Date.isWithinPastDays(date, days: 1) } - } - - // MARK: - Filter by Content Type - - /// Get all video highlights - func getVideos() -> [YouTubeVideo] { - return highlights.compactMap { highlight in - if case .video(let video) = highlight { - return video - } - return nil + + mainPastThreeDaysHighlights = filteredBySport.filter { + guard let date = Date.fullDateFormatter.date(from: $0.publishedAt) else { return false } + return !Date.isWithinPastDays(date, days: 1) && Date.isWithinPastDays(date, days: 3) } - } - - /// Get all article highlights - func getArticles() -> [Article] { - return highlights.compactMap { highlight in - if case .article(let article) = highlight { - return article - } - return nil + + // --- Detailed Page Filters (by sport + search) + detailedTodayHighlights = filteredBySearch.filter { + guard let date = Date.fullDateFormatter.date(from: $0.publishedAt) else { return false } + return Date.isWithinPastDays(date, days: 1) } - } - - /// Filter highlights by content type - func getHighlightsByType(isVideo: Bool) -> [Highlight] { - if isVideo { - return highlights.filter { - if case .video = $0 { - return true - } - return false - } - } else { - return highlights.filter { - if case .article = $0 { - return true - } - return false - } + + detailedPastThreeDaysHighlights = filteredBySearch.filter { + guard let date = Date.fullDateFormatter.date(from: $0.publishedAt) else { return false } + return !Date.isWithinPastDays(date, days: 1) && Date.isWithinPastDays(date, days: 3) } + + // --- “Search All” Page + allHighlightsSearchResults = filteredBySearch } - - // MARK: - Error Handling - - /// Handle error state - func handleError(_ error: ScoreError) { - DispatchQueue.main.async { [weak self] in - self?.dataState = .error(error: error) - self?.isLoading = false - } + + // MARK: - Search & Sport + func filterBySearch(_ query: String) { + searchQuery = query + filter() } - - /// Retry after error - func retry() { - loadHighlights() + + func clearSearch() { + searchQuery = "" + filter() } - - // MARK: - Helper Methods - - /// Get highlight by ID - func getHighlight(by id: String) -> Highlight? { - return highlights.first { $0.id == id } + + func selectSport(_ sport: Sport) { + selectedSport = sport + filter() } - - /// Check if highlight is a video - func isVideo(_ highlight: Highlight) -> Bool { - if case .video = highlight { - return true + + // MARK: - Helpers + private func highlightTitle(_ highlight: Highlight) -> String { + switch highlight { + case .video(let video): return video.title + case .article(let article): return article.title } - return false } - - /// Check if highlight is an article - func isArticle(_ highlight: Highlight) -> Bool { - if case .article = highlight { - return true + + func handleError(_ error: ScoreError) { + DispatchQueue.main.async { + self.dataState = .error(error: error) + self.isLoading = false } - return false } } // MARK: - DataState Extension extension DataState { - /// Check if state is in error state var isError: Bool { - if case .error = self { - return true - } + if case .error = self { return true } return false } - - /// Get error if in error state + var error: ScoreError? { - if case .error(let error) = self { - return error - } + if case .error(let err) = self { return err } return nil } } + +enum HighlightsScope { + case main + case today + case pastThreeDays + case all +} diff --git a/score-ios/Views/DetailedViews/DetailedHighlightView.swift b/score-ios/Views/DetailedViews/DetailedHighlightView.swift index a5c8ae8..578c830 100644 --- a/score-ios/Views/DetailedViews/DetailedHighlightView.swift +++ b/score-ios/Views/DetailedViews/DetailedHighlightView.swift @@ -9,68 +9,88 @@ import SwiftUI struct DetailedHighlightsView: View { @Environment(\.dismiss) private var dismiss + @EnvironmentObject var viewModel: HighlightsViewModel + var title: String - var highlights: [Highlight] + var highlightScope: HighlightsScope var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { + // Custom header ZStack { - Text(title) + 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() - } - } + + 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) + Divider().background(.clear) VStack(alignment: .leading, spacing: 0) { - SearchView(title: "Search \(title)") - .padding(.horizontal, 24) - .padding(.top, 20) + SearchView(title: "Search \(title)", scope: highlightScope) + .padding(.horizontal, 24) + .padding(.top, 20) SportSelectorView() .padding(.top, 20) - VStack{ - ForEach(highlights) { highlight in - HighlightTile(highlight: highlight, width: 360) + // Highlights list + VStack { + ForEach(highlightsForScope, id: \.id) { 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) } + .environmentObject(viewModel) + .onAppear { + if viewModel.hasNotFetchedYet { + viewModel.loadHighlights() + } + + viewModel.clearSearch() + } + .onChange(of: viewModel.selectedSport) { _, _ in + viewModel.filter() + } } + + // MARK: - Helpers + private var highlightsForScope: [Highlight] { + switch highlightScope { + case .today: + return viewModel.mainTodayHighlights + default: + return viewModel.mainPastThreeDaysHighlights + } + } } #Preview { DetailedHighlightsView( title: "Today", - highlights: Highlight.dummyData + highlightScope: .today ) .environmentObject(HighlightsViewModel.shared) } diff --git a/score-ios/Views/ListViews/HighlightTileArticle.swift b/score-ios/Views/ListViews/HighlightTileArticle.swift index f9b693d..bcb1328 100644 --- a/score-ios/Views/ListViews/HighlightTileArticle.swift +++ b/score-ios/Views/ListViews/HighlightTileArticle.swift @@ -94,11 +94,3 @@ struct HighlightTileArticle: View { } } - - -// 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 index aa2cf08..dff9fa5 100644 --- a/score-ios/Views/ListViews/HighlightTileVideo.swift +++ b/score-ios/Views/ListViews/HighlightTileVideo.swift @@ -99,8 +99,3 @@ struct HighlightTileVideo: View { } } } - - -#Preview { - HighlightTileVideo(video: YouTubeVideo.dummyData[0], width: 241) -} diff --git a/score-ios/Views/ListViews/HighlightView.swift b/score-ios/Views/ListViews/HighlightView.swift index 1002acb..9bed219 100644 --- a/score-ios/Views/ListViews/HighlightView.swift +++ b/score-ios/Views/ListViews/HighlightView.swift @@ -20,41 +20,68 @@ struct HighlightView: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 24) .padding(.horizontal, 24) - - SearchView(title: "Search All Highlights") - .padding(.horizontal, 20) - .padding(.top, 12) + + SearchView(title: "Search All Highlights", scope: .all) + .padding(.horizontal, 20) + .padding(.top, 12) SportSelectorView() .padding(.horizontal, 20) .padding(.top, 12) - if viewModel.hasTodayHighlights { - HighlightSectionView(title: "Today", highlights: viewModel.todayHighlights) + if !viewModel.mainTodayHighlights.isEmpty { + HighlightSectionView( + title: "Today", + scope: .today + ) } - if viewModel.hasPastThreeDaysHighlights { - HighlightSectionView(title: "Past 3 Days", highlights: viewModel.pastThreeDaysHighlights) + if !viewModel.mainPastThreeDaysHighlights.isEmpty { + HighlightSectionView( + title: "Past 3 Days", + scope: .pastThreeDays + ) } } } - } - .environmentObject(viewModel) - .onAppear { - if viewModel.hasNotFetchedYet { - viewModel.loadHighlights() + .environmentObject(viewModel) + .onAppear { + if viewModel.hasNotFetchedYet { + viewModel.loadHighlights() + } + + viewModel.clearSearch() + } + .onChange(of: viewModel.selectedSport) { _, _ in + viewModel.filter() } } } } struct HighlightSectionView: View { + @EnvironmentObject var viewModel: HighlightsViewModel + let title: String - let highlights: [Highlight] + let scope: HighlightsScope + private var highlights: [Highlight] { + switch scope { + case .today: + return viewModel.mainTodayHighlights + case .pastThreeDays: + return viewModel.mainPastThreeDaysHighlights + default: + return [] // Should not happen on this screen + } + } + var body: some View { VStack(alignment: .leading, spacing: 0) { - NavigationLink(destination: DetailedHighlightsView(title: title, highlights: highlights)) { + NavigationLink(destination: + DetailedHighlightsView(title: title, highlightScope: .today) + .environmentObject(viewModel)) + { HStack { Text(title) .font(Constants.Fonts.subheader) diff --git a/score-ios/Views/ListViews/SearchView.swift b/score-ios/Views/ListViews/SearchView.swift index 7fb5c61..fcb27e7 100644 --- a/score-ios/Views/ListViews/SearchView.swift +++ b/score-ios/Views/ListViews/SearchView.swift @@ -11,6 +11,7 @@ struct SearchView: View { @ObservedObject private var viewModel = HighlightsViewModel.shared @State private var showSearch = false let title: String + let scope: HighlightsScope var body: some View { Button(action: { showSearch = true }) { @@ -31,13 +32,16 @@ struct SearchView: View { } .buttonStyle(PlainButtonStyle()) .fullScreenCover(isPresented: $showSearch) { - SearchViewFullScreen(title: title) + SearchViewFullScreen(title: title, scope: scope) .environmentObject(viewModel) + .onAppear { + viewModel.clearSearch() + } } } } #Preview { - SearchView(title: "Search All Highlights") + SearchView(title: "Search All Highlights", scope: .all) } diff --git a/score-ios/Views/ListViews/SportSelectorView.swift b/score-ios/Views/ListViews/SportSelectorView.swift index 7a9e1b6..15a9cda 100644 --- a/score-ios/Views/ListViews/SportSelectorView.swift +++ b/score-ios/Views/ListViews/SportSelectorView.swift @@ -8,7 +8,7 @@ import SwiftUI struct SportSelectorView: View { - @ObservedObject private var vm = GamesViewModel.shared + @ObservedObject private var vm = HighlightsViewModel.shared @State private var scrollOffset: CGFloat = 0 var body: some View { diff --git a/score-ios/Views/MainViews/SearchViewFullScreen.swift b/score-ios/Views/MainViews/SearchViewFullScreen.swift index d8d8637..47e424a 100644 --- a/score-ios/Views/MainViews/SearchViewFullScreen.swift +++ b/score-ios/Views/MainViews/SearchViewFullScreen.swift @@ -10,6 +10,7 @@ import SwiftUI struct SearchViewFullScreen: View { @EnvironmentObject private var viewModel: HighlightsViewModel let title: String + var scope: HighlightsScope @Environment(\.dismiss) private var dismiss @@ -20,6 +21,20 @@ struct SearchViewFullScreen: View { private let debounceDelay: TimeInterval = 0.8 + private var searchResults: [Highlight] { + let model = viewModel // avoid dynamicMemberLookup confusion + + switch scope { + case .today: + return model.mainTodayHighlights + case .pastThreeDays: + return model.mainPastThreeDaysHighlights + default: + return model.allHighlights + } + } + + var body: some View { VStack(spacing: 0) { // MARK: Header @@ -76,33 +91,22 @@ struct SearchViewFullScreen: View { if searchText.isEmpty { Spacer() } else if viewModel.isLoading { + Spacer() + } else if searchResults.isEmpty { VStack { - Spacer() - - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - .scaleEffect(1.2) - - Spacer() - } - } else if viewModel.filteredHighlights.isEmpty { - VStack { - NoHighlightView() + NoHighlightView() } } else { ScrollView { HStack { - Text("\(viewModel.filteredCount) results") + Text("\(searchResults.count) results") .padding(.top, 12) - .padding(.horizontal, 24) - .font(Constants.Fonts.subheader) - .foregroundStyle(Constants.Colors.gray_text) Spacer() } LazyVStack(alignment: .leading, spacing: 24) { - ForEach(viewModel.filteredHighlights) { highlight in + ForEach(searchResults) { highlight in HighlightTile(highlight: highlight, width: 360) .padding(.horizontal, 24) } @@ -113,8 +117,14 @@ struct SearchViewFullScreen: View { } .onAppear { isSearchFieldFocused = true + searchText = viewModel.searchQuery + viewModel.filter() + } + .onDisappear { + viewModel.clearSearch() } } + // MARK: - Debounce private func debounceSearch(_ text: String) { @@ -124,6 +134,7 @@ struct SearchViewFullScreen: View { DispatchQueue.main.async { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) viewModel.filterBySearch(trimmed) + viewModel.filter() } } @@ -134,6 +145,6 @@ struct SearchViewFullScreen: View { // MARK: - Preview #Preview { - SearchViewFullScreen(title: "Search All Highlights") + SearchViewFullScreen(title: "Search All Highlights", scope: .pastThreeDays) .environmentObject(HighlightsViewModel.shared) } From 34d21673e1da7f724c9c16db52637810d77f0509 Mon Sep 17 00:00:00 2001 From: Zain Bilal Date: Tue, 11 Nov 2025 14:39:08 -0500 Subject: [PATCH 03/12] Added noHiglightView --- score-ios.xcodeproj/project.pbxproj | 8 +- .../HighlightStar.imageset/Contents.json | 23 +++++ .../HighlightStar.imageset/HighlighStarx2.png | Bin 0 -> 3262 bytes .../HighlightStar.imageset/HighlightStar.png | Bin 0 -> 1765 bytes .../HighlightStarx3.png | Bin 0 -> 4883 bytes .../DetailedViews/DetailedHighlightView.swift | 21 +++-- .../Views/DetailedViews/NoHighlightView.swift | 9 +- score-ios/Views/ListViews/HighlightView.swift | 83 ++++++++++-------- .../MainViews/SearchViewFullScreen.swift | 28 +++--- 9 files changed, 106 insertions(+), 66 deletions(-) create mode 100644 score-ios/Resources/Assets.xcassets/HighlightStar.imageset/Contents.json create mode 100644 score-ios/Resources/Assets.xcassets/HighlightStar.imageset/HighlighStarx2.png create mode 100644 score-ios/Resources/Assets.xcassets/HighlightStar.imageset/HighlightStar.png create mode 100644 score-ios/Resources/Assets.xcassets/HighlightStar.imageset/HighlightStarx3.png diff --git a/score-ios.xcodeproj/project.pbxproj b/score-ios.xcodeproj/project.pbxproj index fe2830b..5a3cbee 100644 --- a/score-ios.xcodeproj/project.pbxproj +++ b/score-ios.xcodeproj/project.pbxproj @@ -26,8 +26,8 @@ 7626AD6C2E973D9B002149CD /* HighlightTileVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7626AD662E973D9B002149CD /* HighlightTileVideo.swift */; }; 7626AD6F2E973E08002149CD /* View+CornerRadius.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7626AD6E2E973E08002149CD /* View+CornerRadius.swift */; }; 7665A4072EB00531004A9903 /* HighlightsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7665A4062EB00528004A9903 /* HighlightsViewModel.swift */; }; + 7675D0932EBC0F1D00940292 /* NoHighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7675D0922EBC0F1700940292 /* NoHighlightView.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 */; }; @@ -139,8 +139,8 @@ 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 = ""; }; 7665A4062EB00528004A9903 /* HighlightsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightsViewModel.swift; sourceTree = ""; }; + 7675D0922EBC0F1700940292 /* NoHighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoHighlightView.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 = ""; }; @@ -449,7 +449,7 @@ D86347AE2CDBD2F3003DD8F6 /* PastGameCard.swift */, CE528FF62C979DA000C238B5 /* GameView.swift */, D836AD912CB62C8800BD1545 /* NoGameView.swift */, - 76E679202E9FF69C00C39132 /* NoHighlightView.swift */, + 7675D0922EBC0F1700940292 /* NoHighlightView.swift */, D87882272CC060FC00421F67 /* GameDetailedScoreView.swift */, CE3C9C402D010177008BFB4C /* ScoringSummary.swift */, ); @@ -768,7 +768,6 @@ 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 */, @@ -779,6 +778,7 @@ CE8ED50E2D6C3B8000A274DE /* SportSelectorView.swift in Sources */, CE8ED5082D6C36E200A274DE /* GameListView.swift in Sources */, CE8ED5142D6C42D400A274DE /* GameSectionHeaderView.swift in Sources */, + 7675D0932EBC0F1D00940292 /* NoHighlightView.swift in Sources */, D86347B12CDBFF7C003DD8F6 /* UpcomingGamesView.swift in Sources */, D86347DF2CE98B3C003DD8F6 /* MainTabView.swift in Sources */, 1C87865F2D8CDADC00EBDF74 /* String+Extension.swift in Sources */, diff --git a/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/Contents.json new file mode 100644 index 0000000..f964161 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "HighlightStar.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "kid_star (1).png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "kid_star (2).png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/HighlighStarx2.png b/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/HighlighStarx2.png new file mode 100644 index 0000000000000000000000000000000000000000..669fdcfb9cedfa4a7f212b39347107b063357bac GIT binary patch literal 3262 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 literal 0 HcmV?d00001 diff --git a/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/HighlightStar.png b/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/HighlightStar.png new file mode 100644 index 0000000000000000000000000000000000000000..7e7ee81a31de306b47c5ffd9abab2250defeaf9b GIT binary patch literal 1765 zcmV}Rb~gC6YO!6=lpmzS3doIJT4o-qO+%>ATnjQKWuzE@`Zj&Q$k6 z#8E^Tfv=BLbwTj2qV$_{(x8*Bg<6lD!lbyA%c%_aekwkyIUbKwPa;{uDQT>WJh_~7 z*Urw)iLM>)b;;0`%ORQT80u%jF`SWx&UMi76Srq`1{3<5)O^lZk+I z4~C^up#%GAEq&bt0*tWKYPni!IgvEhV==;5kELE3QQai{hS|0$93XenSm$_hIq9e* zJ*ETcH=F{wob(&a6DRgtuB1Wn|G=p$m$M$*7nZ=%>ri9(cJY$BB4^~L(~|U97KSNa zh!5hf6}NKNZ@EYs`Nop;SQeH%eFlA&;#TdU_%!;sH9euBg(3GPfR< zDquL$%kkxMa$eG;0jUeJmb#fqi-^MIu24LDgvcJ7Tqb1!lBjyUK8U=%8U#45vLeum zw6sJ`0?L~f;*TN+B=Dp=WwU`MYSiQdCQP5@<7v1~{%7jYhB4YQ@&O zU!*ZvB;Flih%ykR1Fcu_e0wyb3p-rOA;ge0Hbj+vE*;8DlnOMV6#U^lvtOOif6O&eY-x9GY?elDCZ5-L}MC>;z5JS0zYO=VG6 zohTicK+=H+xBK_Nc(G9YOw zDaSOvVgyrs?uF-sq_HJ@sH$8=Gel=zkjGiKGE&Ap6LKazSD}9wm||XNRgpGW z4ADGE2>qKn@k$5w=$^>svU#CGVqhg9$A7NPkRGG%>+9>>m?5mk zVvtkury+H!6)&@XD!y=id3m|?(#D3lr?_>gO2totQGObkA>wlzVI{!k#gS<{Bhnzk zl$$XU6#@DZ_DLARI1>_YXCb}0lPGlSqDvr#o7o7-gfz^cLs4>!$K!I5wrH>+tx7u0 zsxN;@vp`nEvodB#8e~G6jjiJvS>~twod_&oL|PTo6m0rrDrr?*4H)-nSgs9}{wHAy{%y2%bbObk$<}G$TNT5beyx4(}X}ne#cM>mCvTj zH=YJ71(Ogwo`yhVq_}65LW->MZ+sZTiZtFgem-qGAa9cG;j)ho_VQZTn@S-iW`nSf zu*6JL8E;0DH)3UJj9Ot*3h7rHXcrE@K>7*T(?#0vRT}&U_%183;wmHb00000NkvXX Hu0mjfR8A^9 literal 0 HcmV?d00001 diff --git a/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/HighlightStarx3.png b/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/HighlightStarx3.png new file mode 100644 index 0000000000000000000000000000000000000000..d7dffeb1f32514553f41ece93ce0a9ab1c385ba8 GIT binary patch literal 4883 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 Date: Tue, 11 Nov 2025 15:08:10 -0500 Subject: [PATCH 04/12] passed HighlightViewModel as environment object --- score-ios/Models/Highlight.swift | 12 ++++++------ score-ios/Views/ListViews/HighlightView.swift | 2 +- score-ios/Views/MainViews/MainTabView.swift | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/score-ios/Models/Highlight.swift b/score-ios/Models/Highlight.swift index 5c6638f..681990a 100644 --- a/score-ios/Models/Highlight.swift +++ b/score-ios/Models/Highlight.swift @@ -59,7 +59,7 @@ extension Highlight { thumbnail: "https://i.ytimg.com/vi/QGHb9heJAco/hqdefault.jpg", b64Thumbnail: nil, url: "https://youtube.com/watch?v=QGHb9heJAco", - publishedAt: "2025-11-05T00:00:00Z", + publishedAt: "2025-11-11T00:00:00Z", sport: .All ) ), @@ -71,7 +71,7 @@ extension Highlight { 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-11-05T00:00:00Z", + publishedAt: "2025-11-11T00:00:00Z", sport: .FieldHockey ) ), @@ -83,7 +83,7 @@ extension Highlight { thumbnail: "https://i.ytimg.com/vi/ABC123def/hqdefault.jpg", b64Thumbnail: nil, url: "https://youtube.com/watch?v=ABC123def", - publishedAt: "2025-11-05T00:00:00Z", + publishedAt: "2025-11-11T00:00:00Z", sport: .Baseball ) ), @@ -95,7 +95,7 @@ extension Highlight { 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-11-05T00:00:00Z", + publishedAt: "2025-11-10T00:00:00Z", sport: .Football ) ), @@ -107,7 +107,7 @@ extension Highlight { thumbnail: "https://i.ytimg.com/vi/XYZ789ghi/hqdefault.jpg", b64Thumbnail: nil, url: "https://youtube.com/watch?v=XYZ789ghi", - publishedAt: "2025-11-04T00:00:00Z", + publishedAt: "2025-11-10T00:00:00Z", sport: .FieldHockey ) ), @@ -119,7 +119,7 @@ extension Highlight { 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-11-04T00:00:00Z", + publishedAt: "2025-11-11T00:00:00Z", sport: .Baseball ) ) diff --git a/score-ios/Views/ListViews/HighlightView.swift b/score-ios/Views/ListViews/HighlightView.swift index f8cdb0a..579f22c 100644 --- a/score-ios/Views/ListViews/HighlightView.swift +++ b/score-ios/Views/ListViews/HighlightView.swift @@ -8,7 +8,7 @@ import SwiftUI struct HighlightView: View { - @ObservedObject private var viewModel = HighlightsViewModel.shared + @EnvironmentObject var viewModel: HighlightsViewModel var body: some View { ScrollView(showsIndicators: false) { diff --git a/score-ios/Views/MainViews/MainTabView.swift b/score-ios/Views/MainViews/MainTabView.swift index 1b0f5a7..adbb84f 100644 --- a/score-ios/Views/MainViews/MainTabView.swift +++ b/score-ios/Views/MainViews/MainTabView.swift @@ -13,6 +13,7 @@ struct MainTabView: View { @Binding var selectedTab: MainTab @StateObject private var gamesViewModel = GamesViewModel.shared + @StateObject private var highlightViewModel = HighlightsViewModel.shared var body: some View { NavigationStack { @@ -23,7 +24,7 @@ struct MainTabView: View { .environmentObject(gamesViewModel) case .highlights: HighlightView() - .environmentObject(gamesViewModel) + .environmentObject(highlightViewModel) case .scores: PastGamesView() .environmentObject(gamesViewModel) From b5cecb1b5c796f5ceb71e4410b6dc956d86ecdbc Mon Sep 17 00:00:00 2001 From: Zain Bilal Date: Tue, 11 Nov 2025 15:52:30 -0500 Subject: [PATCH 05/12] attempt: make filter and search stick to top of screen --- .../DetailedViews/DetailedHighlightView.swift | 62 +++++++++++-------- score-ios/Views/ListViews/HighlightView.swift | 3 +- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/score-ios/Views/DetailedViews/DetailedHighlightView.swift b/score-ios/Views/DetailedViews/DetailedHighlightView.swift index 2274f3c..97b8c7a 100644 --- a/score-ios/Views/DetailedViews/DetailedHighlightView.swift +++ b/score-ios/Views/DetailedViews/DetailedHighlightView.swift @@ -16,7 +16,7 @@ struct DetailedHighlightsView: View { var body: some View { ScrollView { - VStack(alignment: .leading, spacing: 16) { + LazyVStack(alignment: .leading, spacing: 16, pinnedViews: [.sectionHeaders]) { // Custom header ZStack { @@ -38,31 +38,39 @@ struct DetailedHighlightsView: View { Divider().background(.clear) - VStack(alignment: .leading, spacing: 0) { - SearchView(title: "Search \(title)", scope: highlightScope) - .padding(.horizontal, 24) - .padding(.top, 20) - - SportSelectorView() - .padding(.top, 20) - - if(highlightsForScope.isEmpty) { - NoHighlightView() - .frame(maxWidth: .infinity) - .frame(minHeight: UIScreen.main.bounds.height - 350) - // push view to the middle of the screen - } - else{ - VStack { - ForEach(highlightsForScope, id: \.id) { highlight in - HighlightTile(highlight: highlight, width: 360) - .padding(.horizontal, 24) - .padding(.top, 12) + Section( + header: + VStack(alignment: .leading, spacing: 0) { + SearchView(title: "Search \(title)", scope: highlightScope) + .padding(.horizontal, 24) + .padding(.top, 20) + + SportSelectorView() + .padding(.top, 20) + } + .padding(.bottom, 20) + .background(Color.white) + , + content: { + VStack(alignment: .leading, spacing: 0) { + if(highlightsForScope.isEmpty) { + NoHighlightView() + .frame(maxWidth: .infinity) + .frame(minHeight: UIScreen.main.bounds.height - 350) + // push view to the middle of the screen + } + else{ + LazyVStack { + ForEach(highlightsForScope, id: \.id) { highlight in + HighlightTile(highlight: highlight, width: 360) + .padding(.horizontal, 24) + .padding(.top, 12) + } + } } } - .padding(.top, 20) } - } + ) } } .navigationBarBackButtonHidden(true) @@ -87,9 +95,11 @@ struct DetailedHighlightsView: View { private var highlightsForScope: [Highlight] { switch highlightScope { case .today: - return viewModel.mainTodayHighlights + return viewModel.detailedTodayHighlights + case .pastThreeDays: + return viewModel.detailedPastThreeDaysHighlights default: - return viewModel.mainPastThreeDaysHighlights + return viewModel.allHighlights } } } @@ -97,7 +107,7 @@ struct DetailedHighlightsView: View { #Preview { DetailedHighlightsView( title: "Today", - highlightScope: .today + highlightScope: .pastThreeDays ) .environmentObject(HighlightsViewModel.shared) } diff --git a/score-ios/Views/ListViews/HighlightView.swift b/score-ios/Views/ListViews/HighlightView.swift index 579f22c..5401fd1 100644 --- a/score-ios/Views/ListViews/HighlightView.swift +++ b/score-ios/Views/ListViews/HighlightView.swift @@ -88,7 +88,7 @@ struct HighlightSectionView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { NavigationLink(destination: - DetailedHighlightsView(title: title, highlightScope: .today) + DetailedHighlightsView(title: title, highlightScope: scope) .environmentObject(viewModel)) { HStack { @@ -129,4 +129,5 @@ struct HighlightSectionView: View { #Preview { HighlightView() + .environmentObject(HighlightsViewModel.shared) } From 600efc47f46ea8e6b0bd0507b524a9f6587f2234 Mon Sep 17 00:00:00 2001 From: Zain Bilal Date: Wed, 12 Nov 2025 14:50:58 -0500 Subject: [PATCH 06/12] Added networking for articles --- score-ios.xcodeproj/project.pbxproj | 12 +- score-ios/Models/Article.swift | 12 +- score-ios/Models/GraphQL/Article.graphql | 10 ++ score-ios/Models/GraphQL/YoutubeVideo.graphql | 11 ++ score-ios/Models/GraphQL/schema.graphqls | 12 +- score-ios/Models/Highlight.swift | 36 ------ score-ios/Networking/NetworkManager.swift | 18 ++- score-ios/Utils/Dates.swift | 20 +++- .../ViewModels/HighlightsViewModel.swift | 111 +++++++++++++----- .../ListViews/HighlightTileArticle.swift | 10 +- 10 files changed, 171 insertions(+), 81 deletions(-) create mode 100644 score-ios/Models/GraphQL/Article.graphql create mode 100644 score-ios/Models/GraphQL/YoutubeVideo.graphql diff --git a/score-ios.xcodeproj/project.pbxproj b/score-ios.xcodeproj/project.pbxproj index 5a3cbee..00e3493 100644 --- a/score-ios.xcodeproj/project.pbxproj +++ b/score-ios.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 7626AD6F2E973E08002149CD /* View+CornerRadius.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7626AD6E2E973E08002149CD /* View+CornerRadius.swift */; }; 7665A4072EB00531004A9903 /* HighlightsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7665A4062EB00528004A9903 /* HighlightsViewModel.swift */; }; 7675D0932EBC0F1D00940292 /* NoHighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7675D0922EBC0F1700940292 /* NoHighlightView.swift */; }; + 76AED8512EC50B7F00694C0B /* YoutubeVideo.graphql in Resources */ = {isa = PBXBuildFile; fileRef = 76AED8502EC50B7100694C0B /* YoutubeVideo.graphql */; }; + 76AED8532EC50C2700694C0B /* Article.graphql in Resources */ = {isa = PBXBuildFile; fileRef = 76AED8522EC50C2100694C0B /* Article.graphql */; }; 76D998E92E9F1AF900713EE5 /* SearchViewFullScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76D998E82E9F1AF300713EE5 /* SearchViewFullScreen.swift */; }; B136701ECD164EE9AC64667F /* Article.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CB4DBAE237D47AB882D4EBC /* Article.swift */; }; CE335CD32C922E8D0037F572 /* PrimaryColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE335CD22C922E8D0037F572 /* PrimaryColors.swift */; }; @@ -140,6 +142,8 @@ 7626AD6E2E973E08002149CD /* View+CornerRadius.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+CornerRadius.swift"; sourceTree = ""; }; 7665A4062EB00528004A9903 /* HighlightsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightsViewModel.swift; sourceTree = ""; }; 7675D0922EBC0F1700940292 /* NoHighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoHighlightView.swift; sourceTree = ""; }; + 76AED8502EC50B7100694C0B /* YoutubeVideo.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = YoutubeVideo.graphql; sourceTree = ""; }; + 76AED8522EC50C2100694C0B /* Article.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = Article.graphql; sourceTree = ""; }; 76D998E82E9F1AF300713EE5 /* SearchViewFullScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewFullScreen.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 = ""; }; @@ -527,6 +531,8 @@ children = ( D8DD4E632CFD48E400F2C46E /* Team.graphql */, D891020A2CED6A86004CE226 /* Game.graphql */, + 76AED8522EC50C2100694C0B /* Article.graphql */, + 76AED8502EC50B7100694C0B /* YoutubeVideo.graphql */, D89102062CED6A28004CE226 /* schema.graphqls */, ); path = GraphQL; @@ -634,7 +640,7 @@ packageReferences = ( D89102012CED68D9004CE226 /* XCRemoteSwiftPackageReference "apollo-ios" */, D0A904F32E8DDD990008194B /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, - 76101F682E9743EC006D6EDD /* XCLocalSwiftPackageReference "gameAPI" */, + 761050E32EC50DCC00FD73F8 /* XCLocalSwiftPackageReference "gameAPI" */, ); productRefGroup = CE725D392C89120200386943 /* Products */; projectDirPath = ""; @@ -655,9 +661,11 @@ D8DD4E642CFD48ED00F2C46E /* Team.graphql in Resources */, D891020B2CED6A8E004CE226 /* Game.graphql in Resources */, 2C1375CB2E7233390089EBC7 /* GoogleService-Info.plist in Resources */, + 76AED8512EC50B7F00694C0B /* YoutubeVideo.graphql in Resources */, CE528FE42C96A27500C238B5 /* Poppins-Light.ttf in Resources */, CE528FEE2C96A27500C238B5 /* Poppins-Black.ttf in Resources */, CE528FEC2C96A27500C238B5 /* Poppins-LightItalic.ttf in Resources */, + 76AED8532EC50C2700694C0B /* Article.graphql in Resources */, CE528FEF2C96A27500C238B5 /* Poppins-Thin.ttf in Resources */, CE528FE02C96A27500C238B5 /* Poppins-ExtraLight.ttf in Resources */, CE528FF02C96A27500C238B5 /* Poppins-SemiBold.ttf in Resources */, @@ -1119,7 +1127,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 76101F682E9743EC006D6EDD /* XCLocalSwiftPackageReference "gameAPI" */ = { + 761050E32EC50DCC00FD73F8 /* XCLocalSwiftPackageReference "gameAPI" */ = { isa = XCLocalSwiftPackageReference; relativePath = gameAPI; }; diff --git a/score-ios/Models/Article.swift b/score-ios/Models/Article.swift index 45dee41..198236b 100644 --- a/score-ios/Models/Article.swift +++ b/score-ios/Models/Article.swift @@ -6,14 +6,13 @@ // import Foundation +import GameAPI 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 sport: Sport @@ -25,4 +24,13 @@ struct Article: Identifiable { } return publishedAt } + + init(from gqlArticle: ArticlesQuery.Data.Article) { + self.id = gqlArticle.id ?? UUID().uuidString + self.image = gqlArticle.image ?? "" + self.title = gqlArticle.title + self.url = gqlArticle.url + self.publishedAt = gqlArticle.publishedAt + self.sport = Sport(normalizedValue: gqlArticle.sportsType) ?? .All + } } diff --git a/score-ios/Models/GraphQL/Article.graphql b/score-ios/Models/GraphQL/Article.graphql new file mode 100644 index 0000000..2806293 --- /dev/null +++ b/score-ios/Models/GraphQL/Article.graphql @@ -0,0 +1,10 @@ +query Articles($sportsType: String) { + articles(sportsType: $sportsType) { + id + title + image + sportsType + publishedAt + url + } +} diff --git a/score-ios/Models/GraphQL/YoutubeVideo.graphql b/score-ios/Models/GraphQL/YoutubeVideo.graphql new file mode 100644 index 0000000..dadd05a --- /dev/null +++ b/score-ios/Models/GraphQL/YoutubeVideo.graphql @@ -0,0 +1,11 @@ +query YoutubeVideos { + youtubeVideos { + id + title + description + thumbnail + b64Thumbnail + url + publishedAt + } +} diff --git a/score-ios/Models/GraphQL/schema.graphqls b/score-ios/Models/GraphQL/schema.graphqls index 3433c92..9bacad1 100644 --- a/score-ios/Models/GraphQL/schema.graphqls +++ b/score-ios/Models/GraphQL/schema.graphqls @@ -38,6 +38,7 @@ type Query { gamesBySport(sport: String!): [GameType] gamesByGender(gender: String!): [GameType] gamesBySportGender(sport: String!, gender: String!): [GameType] + gamesByDate(startDate: DateTime!, endDate: DateTime!): [GameType] teams: [TeamType] team(id: String!): TeamType teamByName(name: String!): TeamType @@ -72,6 +73,7 @@ Attributes: - thumbnail: The URL of the video's thumbnail. - url: The URL to the video. - published_at: The date and time the video was published. + - duration: The duration of the video (optional). """ type YoutubeVideoType { id: String @@ -81,6 +83,7 @@ type YoutubeVideoType { b64Thumbnail: String! url: String! publishedAt: String! + duration: String } """ @@ -163,6 +166,13 @@ type TeamType { name: String! } +""" +The `DateTime` scalar type represents a DateTime +value as specified by +[iso8601](https://en.wikipedia.org/wiki/ISO_8601). +""" +scalar DateTime + 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!, ticketLink: String, time: String!, utcDate: String): CreateGame @@ -171,7 +181,7 @@ type Mutation { 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 + 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 diff --git a/score-ios/Models/Highlight.swift b/score-ios/Models/Highlight.swift index 681990a..b265650 100644 --- a/score-ios/Models/Highlight.swift +++ b/score-ios/Models/Highlight.swift @@ -63,18 +63,6 @@ extension Highlight { sport: .All ) ), - .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-11-11T00:00:00Z", - sport: .FieldHockey - ) - ), .video( YouTubeVideo( id: "ABC123def", @@ -87,18 +75,6 @@ extension Highlight { sport: .Baseball ) ), - .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-11-10T00:00:00Z", - sport: .Football - ) - ), .video( YouTubeVideo( id: "XYZ789ghi", @@ -110,18 +86,6 @@ extension Highlight { publishedAt: "2025-11-10T00:00:00Z", sport: .FieldHockey ) - ), - .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-11-11T00:00:00Z", - sport: .Baseball - ) ) ] } diff --git a/score-ios/Networking/NetworkManager.swift b/score-ios/Networking/NetworkManager.swift index 57bace5..3559f98 100644 --- a/score-ios/Networking/NetworkManager.swift +++ b/score-ios/Networking/NetworkManager.swift @@ -46,5 +46,21 @@ class NetworkManager { } } } - + func fetchArticles(completion: @escaping ([ArticlesQuery.Data.Article]?, Error?) -> Void) { + let query = ArticlesQuery(sportsType: nil) + + apolloClient.fetch(query: query) { result in + switch result { + case .success(let graphQLResult): + if let articlesData = graphQLResult.data?.articles?.compactMap({ $0 }) { + completion(articlesData, nil) + } else if let errors = graphQLResult.errors { + let errorDescription = errors.map { $0.localizedDescription }.joined(separator: "\n") + completion(nil, NSError(domain: "GraphQL", code: 0, userInfo: [NSLocalizedDescriptionKey: errorDescription])) + } + case .failure(let error): + completion(nil, error) + } + } + } } diff --git a/score-ios/Utils/Dates.swift b/score-ios/Utils/Dates.swift index 6dd25f4..4773a56 100644 --- a/score-ios/Utils/Dates.swift +++ b/score-ios/Utils/Dates.swift @@ -39,11 +39,27 @@ extension Date { return calendar.date(from: components) ?? Date() } + /// Formatter for "yyyy-MM-dd HH:mm:ss" strings + static var articleDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = .current // Assumes dates are in local time + return formatter + }() + + /// Checks if a date is any time today. + static func isToday(_ date: Date) -> Bool { + return Calendar.current.isDateInToday(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 } + let calendar = Calendar.current + let startOfToday = calendar.startOfDay(for: Date()) + guard let pastDate = calendar.date(byAdding: .day, value: -days, to: startOfToday) else { return false } - return date >= pastDate && date < Calendar.current.startOfDay(for: Date()) + return date >= pastDate && date < startOfToday } static func parseDate(dateString: String, timeString: String) -> Date { diff --git a/score-ios/ViewModels/HighlightsViewModel.swift b/score-ios/ViewModels/HighlightsViewModel.swift index 6abfab0..f880641 100644 --- a/score-ios/ViewModels/HighlightsViewModel.swift +++ b/score-ios/ViewModels/HighlightsViewModel.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import GameAPI class HighlightsViewModel: ObservableObject { // MARK: - Published Properties @@ -24,6 +25,9 @@ class HighlightsViewModel: ObservableObject { @Published var selectedSport: Sport = .All @Published var sportSelectorOffset: CGFloat = 0 @Published var currentScope: HighlightsScope = .main + + // MARK: - Private Properties + private var privateAllHighlights: [Highlight] = [] // MARK: - Singleton static let shared = HighlightsViewModel() @@ -34,20 +38,77 @@ class HighlightsViewModel: ObservableObject { // MARK: - Loading func loadHighlights() { - dataState = .loading - isLoading = true - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.allHighlights = Highlight.dummyData - self.filter() - self.dataState = .success - self.isLoading = false + dataState = .loading + isLoading = true + + self.allHighlights.removeAll() + self.mainTodayHighlights.removeAll() + self.mainPastThreeDaysHighlights.removeAll() + self.detailedTodayHighlights.removeAll() + self.detailedPastThreeDaysHighlights.removeAll() + self.allHighlightsSearchResults.removeAll() + + print("calling network manager") + NetworkManager.shared.fetchArticles() { [weak self] networkArticles, error in + guard let self = self else { return } + + DispatchQueue.main.async { + self.isLoading = false + + if let error = error { + print("Error in fetchArticles: \(error.localizedDescription)") + self.handleError(.networkError) + return + } + + guard let networkArticles = networkArticles, !networkArticles.isEmpty else { + self.handleError(ScoreError.emptyData) + return + } + + print("processing higlights") + self.processHighlights(networkArticles) + } + } } - } - func refreshHighlights() { + func retryFetch() { loadHighlights() } + + /** + * Converts network data to local models, sorts, and filters. + */ + private func processHighlights(_ articleDataArray: [ArticlesQuery.Data.Article]) { + let localArticles = articleDataArray.map { Article(from: $0) } + self.privateAllHighlights = localArticles.map { Highlight.article($0) } + self.allHighlights = self.uniqueHighlights(from: self.privateAllHighlights) + self.allHighlights.sort(by: { $0.publishedAt > $1.publishedAt }) + self.filter() + self.dataState = .success + print(allHighlights) + print("processed higlights") + } + + /** + * Function to filter out duplicate highlights by ID. + */ + private func uniqueHighlights(from highlights: [Highlight]) -> [Highlight] { + var uniqueHighlights: [Highlight] = [] + var seenIDs: Set = [] + + for highlight in highlights { + if case .article(let article) = highlight { + if !seenIDs.contains(article.id) { + uniqueHighlights.append(highlight) + seenIDs.insert(article.id) + } + } + // TODO: Add case for .video when ready + } + + return uniqueHighlights + } // MARK: - Filtering func filter() { @@ -68,26 +129,25 @@ class HighlightsViewModel: ObservableObject { } } - // --- Main Page Filters (by sport only) mainTodayHighlights = filteredBySport.filter { - guard let date = Date.fullDateFormatter.date(from: $0.publishedAt) else { return false } - return Date.isWithinPastDays(date, days: 1) + guard let date = Date.articleDateFormatter.date(from: $0.publishedAt) else { return false } + return Date.isToday(date) } mainPastThreeDaysHighlights = filteredBySport.filter { - guard let date = Date.fullDateFormatter.date(from: $0.publishedAt) else { return false } - return !Date.isWithinPastDays(date, days: 1) && Date.isWithinPastDays(date, days: 3) + guard let date = Date.articleDateFormatter.date(from: $0.publishedAt) else { return false } + return Date.isWithinPastDays(date, days: 3) } // --- Detailed Page Filters (by sport + search) detailedTodayHighlights = filteredBySearch.filter { - guard let date = Date.fullDateFormatter.date(from: $0.publishedAt) else { return false } - return Date.isWithinPastDays(date, days: 1) + guard let date = Date.articleDateFormatter.date(from: $0.publishedAt) else { return false } + return Date.isToday(date) } detailedPastThreeDaysHighlights = filteredBySearch.filter { - guard let date = Date.fullDateFormatter.date(from: $0.publishedAt) else { return false } - return !Date.isWithinPastDays(date, days: 1) && Date.isWithinPastDays(date, days: 3) + guard let date = Date.articleDateFormatter.date(from: $0.publishedAt) else { return false } + return Date.isWithinPastDays(date, days: 3) } // --- “Search All” Page @@ -126,19 +186,6 @@ class HighlightsViewModel: ObservableObject { } } -// MARK: - DataState Extension -extension DataState { - var isError: Bool { - if case .error = self { return true } - return false - } - - var error: ScoreError? { - if case .error(let err) = self { return err } - return nil - } -} - enum HighlightsScope { case main case today diff --git a/score-ios/Views/ListViews/HighlightTileArticle.swift b/score-ios/Views/ListViews/HighlightTileArticle.swift index bcb1328..59870f3 100644 --- a/score-ios/Views/ListViews/HighlightTileArticle.swift +++ b/score-ios/Views/ListViews/HighlightTileArticle.swift @@ -62,11 +62,11 @@ struct HighlightTileArticle: View { // Source and date at bottom HStack { - Text(article.source) - .font(.subheadline) - .fontWeight(.bold) - .foregroundColor(Constants.Colors.white) - .underline() +// Text(article.source) +// .font(.subheadline) +// .fontWeight(.bold) +// .foregroundColor(Constants.Colors.white) +// .underline() Image(systemName: "arrow.up.right") .foregroundStyle(Constants.Colors.white) From f26e3122d6be1a567ce21e130f9a5cf60c205732 Mon Sep 17 00:00:00 2001 From: Zain Bilal Date: Thu, 13 Nov 2025 19:31:22 -0500 Subject: [PATCH 07/12] Implemented networking for YouTubeVideos --- score-ios/Models/GraphQL/YoutubeVideo.graphql | 1 + score-ios/Models/Highlight.swift | 41 ---------------- score-ios/Models/YouTubeVideo.swift | 15 ++++++ score-ios/Networking/NetworkManager.swift | 19 ++++++++ .../ViewModels/HighlightsViewModel.swift | 47 ++++++++++++++----- .../DetailedViews/DetailedHighlightView.swift | 4 +- .../Views/ListViews/HighlightTileVideo.swift | 7 +-- .../MainViews/SearchViewFullScreen.swift | 3 +- 8 files changed, 80 insertions(+), 57 deletions(-) diff --git a/score-ios/Models/GraphQL/YoutubeVideo.graphql b/score-ios/Models/GraphQL/YoutubeVideo.graphql index dadd05a..6a9ce1a 100644 --- a/score-ios/Models/GraphQL/YoutubeVideo.graphql +++ b/score-ios/Models/GraphQL/YoutubeVideo.graphql @@ -7,5 +7,6 @@ query YoutubeVideos { b64Thumbnail url publishedAt + duration } } diff --git a/score-ios/Models/Highlight.swift b/score-ios/Models/Highlight.swift index b265650..0e7e66d 100644 --- a/score-ios/Models/Highlight.swift +++ b/score-ios/Models/Highlight.swift @@ -48,44 +48,3 @@ enum Highlight: Identifiable { } } -// 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-11-11T00:00:00Z", - sport: .All - ) - ), - .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-11-11T00:00:00Z", - sport: .Baseball - ) - ), - .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-11-10T00:00:00Z", - sport: .FieldHockey - ) - ) - ] -} diff --git a/score-ios/Models/YouTubeVideo.swift b/score-ios/Models/YouTubeVideo.swift index 15dd934..087a278 100644 --- a/score-ios/Models/YouTubeVideo.swift +++ b/score-ios/Models/YouTubeVideo.swift @@ -6,6 +6,7 @@ // import Foundation +import GameAPI struct YouTubeVideo: Identifiable { var id: String @@ -16,6 +17,7 @@ struct YouTubeVideo: Identifiable { var url: String var publishedAt: String var sport: Sport + var duration: String? // Format publishedAt -> MM/dd or similar var formattedDate: String { @@ -26,4 +28,17 @@ struct YouTubeVideo: Identifiable { } return publishedAt } + + init(from gqlYouTubeVideo: YoutubeVideosQuery.Data.YoutubeVideo) { + self.id = gqlYouTubeVideo.id ?? UUID().uuidString + self.title = gqlYouTubeVideo.title + self.description = gqlYouTubeVideo.description + self.thumbnail = gqlYouTubeVideo.thumbnail + self.b64Thumbnail = gqlYouTubeVideo.b64Thumbnail + self.url = gqlYouTubeVideo.url + self.publishedAt = gqlYouTubeVideo.publishedAt + self.sport = .All + self.duration = gqlYouTubeVideo.duration + print(publishedAt) + } } diff --git a/score-ios/Networking/NetworkManager.swift b/score-ios/Networking/NetworkManager.swift index 3559f98..b8fbcdb 100644 --- a/score-ios/Networking/NetworkManager.swift +++ b/score-ios/Networking/NetworkManager.swift @@ -46,6 +46,7 @@ class NetworkManager { } } } + func fetchArticles(completion: @escaping ([ArticlesQuery.Data.Article]?, Error?) -> Void) { let query = ArticlesQuery(sportsType: nil) @@ -63,4 +64,22 @@ class NetworkManager { } } } + + func fetchYouTubeVideos(completion: @escaping ([YoutubeVideosQuery.Data.YoutubeVideo]?, Error?) -> Void) { + let query = YoutubeVideosQuery() + + apolloClient.fetch(query: query) { result in + switch result { + case .success(let graphQLResult): + if let youTubeVideoData = graphQLResult.data?.youtubeVideos?.compactMap({ $0 }) { + completion(youTubeVideoData, nil) + } else if let errors = graphQLResult.errors { + let errorDescription = errors.map { $0.localizedDescription }.joined(separator: "\n") + completion(nil, NSError(domain: "GraphQL", code: 0, userInfo: [NSLocalizedDescriptionKey: errorDescription])) + } + case .failure(let error): + completion(nil, error) + } + } + } } diff --git a/score-ios/ViewModels/HighlightsViewModel.swift b/score-ios/ViewModels/HighlightsViewModel.swift index f880641..e29870b 100644 --- a/score-ios/ViewModels/HighlightsViewModel.swift +++ b/score-ios/ViewModels/HighlightsViewModel.swift @@ -48,7 +48,6 @@ class HighlightsViewModel: ObservableObject { self.detailedPastThreeDaysHighlights.removeAll() self.allHighlightsSearchResults.removeAll() - print("calling network manager") NetworkManager.shared.fetchArticles() { [weak self] networkArticles, error in guard let self = self else { return } @@ -66,11 +65,31 @@ class HighlightsViewModel: ObservableObject { return } - print("processing higlights") - self.processHighlights(networkArticles) + self.processHighlights(networkArticles, []) } } + + NetworkManager.shared.fetchYouTubeVideos() { [weak self] networkYouTubeVideos, error in + guard let self = self else { return } + + DispatchQueue.main.async { + self.isLoading = false + + if let error = error { + print("Error in fetchArticles: \(error.localizedDescription)") + self.handleError(.networkError) + return + } + + guard let networkYouTubeVideos = networkYouTubeVideos, !networkYouTubeVideos.isEmpty else { + self.handleError(ScoreError.emptyData) + return + } + + self.processHighlights([], networkYouTubeVideos) + } } + } func retryFetch() { loadHighlights() @@ -79,15 +98,15 @@ class HighlightsViewModel: ObservableObject { /** * Converts network data to local models, sorts, and filters. */ - private func processHighlights(_ articleDataArray: [ArticlesQuery.Data.Article]) { + private func processHighlights(_ articleDataArray: [ArticlesQuery.Data.Article], _ youTubeVideoDataArray: [YoutubeVideosQuery.Data.YoutubeVideo]) { let localArticles = articleDataArray.map { Article(from: $0) } - self.privateAllHighlights = localArticles.map { Highlight.article($0) } + let localYouTubeVideos = youTubeVideoDataArray.map {YouTubeVideo(from: $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() self.dataState = .success - print(allHighlights) - print("processed higlights") } /** @@ -95,16 +114,22 @@ class HighlightsViewModel: ObservableObject { */ private func uniqueHighlights(from highlights: [Highlight]) -> [Highlight] { var uniqueHighlights: [Highlight] = [] - var seenIDs: Set = [] + var seenArticleIDs: Set = [] + var seenVideoIDs: Set = [] for highlight in highlights { if case .article(let article) = highlight { - if !seenIDs.contains(article.id) { + if !seenArticleIDs.contains(article.id) { + uniqueHighlights.append(highlight) + seenArticleIDs.insert(article.id) + } + } + if case .video(let video) = highlight { + if !seenVideoIDs.contains(video.id) { uniqueHighlights.append(highlight) - seenIDs.insert(article.id) + seenVideoIDs.insert(video.id) } } - // TODO: Add case for .video when ready } return uniqueHighlights diff --git a/score-ios/Views/DetailedViews/DetailedHighlightView.swift b/score-ios/Views/DetailedViews/DetailedHighlightView.swift index 97b8c7a..91379c2 100644 --- a/score-ios/Views/DetailedViews/DetailedHighlightView.swift +++ b/score-ios/Views/DetailedViews/DetailedHighlightView.swift @@ -49,7 +49,7 @@ struct DetailedHighlightsView: View { .padding(.top, 20) } .padding(.bottom, 20) - .background(Color.white) + .cornerRadius(12, corners: [.bottomLeft, .bottomRight]) , content: { VStack(alignment: .leading, spacing: 0) { @@ -71,6 +71,8 @@ struct DetailedHighlightsView: View { } } ) + .background(Color.white) + .edgesIgnoringSafeArea(.top) } } .navigationBarBackButtonHidden(true) diff --git a/score-ios/Views/ListViews/HighlightTileVideo.swift b/score-ios/Views/ListViews/HighlightTileVideo.swift index dff9fa5..a8dd354 100644 --- a/score-ios/Views/ListViews/HighlightTileVideo.swift +++ b/score-ios/Views/ListViews/HighlightTileVideo.swift @@ -43,9 +43,10 @@ struct HighlightTileVideo: View { HStack(spacing: 2) { Image(systemName: "play.fill") .font(.caption2) - - Text("1:25") - .font(.caption) + if let duration = video.duration{ + Text(duration) + .font(.caption) + } } .fontWeight(.heavy) .foregroundStyle(.white) diff --git a/score-ios/Views/MainViews/SearchViewFullScreen.swift b/score-ios/Views/MainViews/SearchViewFullScreen.swift index 582b94c..d0d790d 100644 --- a/score-ios/Views/MainViews/SearchViewFullScreen.swift +++ b/score-ios/Views/MainViews/SearchViewFullScreen.swift @@ -90,6 +90,8 @@ struct SearchViewFullScreen: View { SportSelectorView() .padding(.horizontal, 20) .padding(.top, 12) + .padding(.bottom, 20) + .cornerRadius(12, corners: [.bottomLeft, .bottomRight]) // MARK: Results if viewModel.isLoading { @@ -103,7 +105,6 @@ struct SearchViewFullScreen: View { ScrollView { HStack { Text("\(searchResults.count) results") - .padding(.top, 12) .padding(.horizontal, 24) .font(Constants.Fonts.subheader) .foregroundStyle(Constants.Colors.gray_text) From 9d73e01d2137d57e74bcb88fd1bb8c92e4aa87ae Mon Sep 17 00:00:00 2001 From: Zain Bilal Date: Thu, 13 Nov 2025 20:24:00 -0500 Subject: [PATCH 08/12] Changed fetch calls to be async --- score-ios/Models/YouTubeVideo.swift | 1 - score-ios/Networking/NetworkManager.swift | 24 +++++++ .../ViewModels/HighlightsViewModel.swift | 70 +++++-------------- .../MainViews/SearchViewFullScreen.swift | 2 +- 4 files changed, 44 insertions(+), 53 deletions(-) diff --git a/score-ios/Models/YouTubeVideo.swift b/score-ios/Models/YouTubeVideo.swift index 087a278..a4aec80 100644 --- a/score-ios/Models/YouTubeVideo.swift +++ b/score-ios/Models/YouTubeVideo.swift @@ -39,6 +39,5 @@ struct YouTubeVideo: Identifiable { self.publishedAt = gqlYouTubeVideo.publishedAt self.sport = .All self.duration = gqlYouTubeVideo.duration - print(publishedAt) } } diff --git a/score-ios/Networking/NetworkManager.swift b/score-ios/Networking/NetworkManager.swift index b8fbcdb..b40db61 100644 --- a/score-ios/Networking/NetworkManager.swift +++ b/score-ios/Networking/NetworkManager.swift @@ -82,4 +82,28 @@ class NetworkManager { } } } + + func fetchArticles() async throws -> [ArticlesQuery.Data.Article] { + try await withCheckedThrowingContinuation { continuation in + fetchArticles { articles, error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: articles ?? []) + } + } + } + } + + func fetchYouTubeVideos() async throws -> [YoutubeVideosQuery.Data.YoutubeVideo] { + try await withCheckedThrowingContinuation { continuation in + fetchYouTubeVideos { videos, error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: videos ?? []) + } + } + } + } } diff --git a/score-ios/ViewModels/HighlightsViewModel.swift b/score-ios/ViewModels/HighlightsViewModel.swift index e29870b..eb3860e 100644 --- a/score-ios/ViewModels/HighlightsViewModel.swift +++ b/score-ios/ViewModels/HighlightsViewModel.swift @@ -9,10 +9,10 @@ import Foundation import SwiftUI import GameAPI +@MainActor class HighlightsViewModel: ObservableObject { // MARK: - Published Properties @Published var dataState: DataState = .idle - @Published var isLoading: Bool = false @Published var allHighlights: [Highlight] = [] @Published var mainTodayHighlights: [Highlight] = [] @@ -38,55 +38,25 @@ class HighlightsViewModel: ObservableObject { // MARK: - Loading func loadHighlights() { - dataState = .loading - isLoading = true - - self.allHighlights.removeAll() - self.mainTodayHighlights.removeAll() - self.mainPastThreeDaysHighlights.removeAll() - self.detailedTodayHighlights.removeAll() - self.detailedPastThreeDaysHighlights.removeAll() - self.allHighlightsSearchResults.removeAll() - - NetworkManager.shared.fetchArticles() { [weak self] networkArticles, error in - guard let self = self else { return } - - DispatchQueue.main.async { - self.isLoading = false - - if let error = error { - print("Error in fetchArticles: \(error.localizedDescription)") - self.handleError(.networkError) - return - } - - guard let networkArticles = networkArticles, !networkArticles.isEmpty else { - self.handleError(ScoreError.emptyData) - return - } - - self.processHighlights(networkArticles, []) - } - } + dataState = .loading - NetworkManager.shared.fetchYouTubeVideos() { [weak self] networkYouTubeVideos, error in - guard let self = self else { return } - - DispatchQueue.main.async { - self.isLoading = false - - if let error = error { - print("Error in fetchArticles: \(error.localizedDescription)") - self.handleError(.networkError) - return - } - - guard let networkYouTubeVideos = networkYouTubeVideos, !networkYouTubeVideos.isEmpty else { - self.handleError(ScoreError.emptyData) - return - } - - self.processHighlights([], networkYouTubeVideos) + self.allHighlights.removeAll() + self.mainTodayHighlights.removeAll() + self.mainPastThreeDaysHighlights.removeAll() + self.detailedTodayHighlights.removeAll() + self.detailedPastThreeDaysHighlights.removeAll() + self.allHighlightsSearchResults.removeAll() + + Task { + do { + async let articles = NetworkManager.shared.fetchArticles() + async let videos = NetworkManager.shared.fetchYouTubeVideos() + + let (articleData, videoData) = try await (articles, videos) + + processHighlights(articleData, videoData) + } catch { + handleError(.networkError) } } } @@ -131,7 +101,6 @@ class HighlightsViewModel: ObservableObject { } } } - return uniqueHighlights } @@ -206,7 +175,6 @@ class HighlightsViewModel: ObservableObject { func handleError(_ error: ScoreError) { DispatchQueue.main.async { self.dataState = .error(error: error) - self.isLoading = false } } } diff --git a/score-ios/Views/MainViews/SearchViewFullScreen.swift b/score-ios/Views/MainViews/SearchViewFullScreen.swift index d0d790d..c10447d 100644 --- a/score-ios/Views/MainViews/SearchViewFullScreen.swift +++ b/score-ios/Views/MainViews/SearchViewFullScreen.swift @@ -94,7 +94,7 @@ struct SearchViewFullScreen: View { .cornerRadius(12, corners: [.bottomLeft, .bottomRight]) // MARK: Results - if viewModel.isLoading { + if viewModel.dataState == .loading { } else if !searchText.isEmpty && searchResults.isEmpty { NoHighlightView() From c5517a6073bee2a65ffea318ad3bd0576e4f1ff0 Mon Sep 17 00:00:00 2001 From: Zain Bilal Date: Thu, 13 Nov 2025 20:27:03 -0500 Subject: [PATCH 09/12] made all viewModel access patterns EnvironmentObject --- score-ios/Views/ListViews/SearchView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/score-ios/Views/ListViews/SearchView.swift b/score-ios/Views/ListViews/SearchView.swift index fcb27e7..a9722d9 100644 --- a/score-ios/Views/ListViews/SearchView.swift +++ b/score-ios/Views/ListViews/SearchView.swift @@ -8,7 +8,7 @@ import SwiftUI struct SearchView: View { - @ObservedObject private var viewModel = HighlightsViewModel.shared + @EnvironmentObject private var viewModel: HighlightsViewModel @State private var showSearch = false let title: String let scope: HighlightsScope @@ -38,6 +38,7 @@ struct SearchView: View { viewModel.clearSearch() } } + .environmentObject(viewModel) } } From 453f6d9bf23b496df0152ce44da55692f90df749 Mon Sep 17 00:00:00 2001 From: Zain Bilal Date: Wed, 3 Dec 2025 18:00:43 -0500 Subject: [PATCH 10/12] fixed date formatting --- score-ios/Utils/Dates.swift | 17 ++++++++++------- score-ios/ViewModels/HighlightsViewModel.swift | 11 +++++++---- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/score-ios/Utils/Dates.swift b/score-ios/Utils/Dates.swift index 4773a56..352324b 100644 --- a/score-ios/Utils/Dates.swift +++ b/score-ios/Utils/Dates.swift @@ -39,29 +39,32 @@ extension Date { return calendar.date(from: components) ?? Date() } - /// Formatter for "yyyy-MM-dd HH:mm:ss" strings - static var articleDateFormatter: DateFormatter = { + /// Formatter for "yyyy-MM-dd'T'HH:mm:ssXXXXX" strings + static var highlightDateFormatter: DateFormatter = { let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX" // ISO 8601 format formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = .current // Assumes dates are in local time + formatter.timeZone = .current return formatter }() /// Checks if a date is any time today. static func isToday(_ date: Date) -> Bool { - return Calendar.current.isDateInToday(date) + Calendar.current.isDateInToday(date) } // Return true if 'date' is within 'days' from today static func isWithinPastDays(_ date: Date, days: Int) -> Bool { let calendar = Calendar.current let startOfToday = calendar.startOfDay(for: Date()) - guard let pastDate = calendar.date(byAdding: .day, value: -days, to: startOfToday) else { return false } + + guard let pastDate = calendar.date(byAdding: .day, value: -days, to: startOfToday) else { + return false + } return date >= pastDate && date < startOfToday } - + static func parseDate(dateString: String, timeString: String) -> Date { // Set up date formatter let dateFormatter = DateFormatter() diff --git a/score-ios/ViewModels/HighlightsViewModel.swift b/score-ios/ViewModels/HighlightsViewModel.swift index eb3860e..95cf272 100644 --- a/score-ios/ViewModels/HighlightsViewModel.swift +++ b/score-ios/ViewModels/HighlightsViewModel.swift @@ -76,6 +76,9 @@ class HighlightsViewModel: ObservableObject { self.allHighlights = self.uniqueHighlights(from: self.privateAllHighlights) self.allHighlights.sort(by: { $0.publishedAt > $1.publishedAt }) self.filter() + + print(localArticles.map{$0.publishedAt}) + print(localYouTubeVideos.map{$0.publishedAt}) self.dataState = .success } @@ -124,23 +127,23 @@ class HighlightsViewModel: ObservableObject { } mainTodayHighlights = filteredBySport.filter { - guard let date = Date.articleDateFormatter.date(from: $0.publishedAt) else { return false } + guard let date = Date.highlightDateFormatter.date(from: $0.publishedAt) else { return false } return Date.isToday(date) } mainPastThreeDaysHighlights = filteredBySport.filter { - guard let date = Date.articleDateFormatter.date(from: $0.publishedAt) else { return false } + guard let date = Date.highlightDateFormatter.date(from: $0.publishedAt) else { return false } return Date.isWithinPastDays(date, days: 3) } // --- Detailed Page Filters (by sport + search) detailedTodayHighlights = filteredBySearch.filter { - guard let date = Date.articleDateFormatter.date(from: $0.publishedAt) else { return false } + guard let date = Date.highlightDateFormatter.date(from: $0.publishedAt) else { return false } return Date.isToday(date) } detailedPastThreeDaysHighlights = filteredBySearch.filter { - guard let date = Date.articleDateFormatter.date(from: $0.publishedAt) else { return false } + guard let date = Date.highlightDateFormatter.date(from: $0.publishedAt) else { return false } return Date.isWithinPastDays(date, days: 3) } From 6f4b9761a5873bbaaab98ea0272dd9da4926d19c Mon Sep 17 00:00:00 2001 From: Zain Bilal Date: Wed, 3 Dec 2025 21:21:30 -0500 Subject: [PATCH 11/12] Code style and naming fixes --- score-ios/Models/Highlight.swift | 8 +++---- .../HighlightStar.imageset/Contents.json | 4 ++-- ...HighlighStarx2.png => HighlightStarx2.png} | Bin .../Image.imageset/Contents.json | 20 ++++++++++++++++++ .../DetailedViews/DetailedHighlightView.swift | 1 + .../Views/DetailedViews/NoHighlightView.swift | 3 +++ .../ListViews/HighlightTileArticle.swift | 10 --------- score-ios/Views/ListViews/HighlightView.swift | 6 +----- 8 files changed, 31 insertions(+), 21 deletions(-) rename score-ios/Resources/Assets.xcassets/HighlightStar.imageset/{HighlighStarx2.png => HighlightStarx2.png} (100%) create mode 100644 score-ios/Resources/Assets.xcassets/Image.imageset/Contents.json diff --git a/score-ios/Models/Highlight.swift b/score-ios/Models/Highlight.swift index 0e7e66d..7ce71c7 100644 --- a/score-ios/Models/Highlight.swift +++ b/score-ios/Models/Highlight.swift @@ -29,8 +29,8 @@ enum Highlight: Identifiable { } } - var title: String{ - switch self{ + var title: String { + switch self { case .article(let article): return article.title case .video(let video): @@ -38,8 +38,8 @@ enum Highlight: Identifiable { } } - var sport: Sport{ - switch self{ + var sport: Sport { + switch self { case .article(let article): return article.sport case .video(let video): diff --git a/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/Contents.json index f964161..a999c7e 100644 --- a/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/Contents.json +++ b/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/Contents.json @@ -6,12 +6,12 @@ "scale" : "1x" }, { - "filename" : "kid_star (1).png", + "filename" : "HighlightStarx2.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "kid_star (2).png", + "filename" : "HighlightStarx3.png", "idiom" : "universal", "scale" : "3x" } diff --git a/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/HighlighStarx2.png b/score-ios/Resources/Assets.xcassets/HighlightStar.imageset/HighlightStarx2.png similarity index 100% rename from score-ios/Resources/Assets.xcassets/HighlightStar.imageset/HighlighStarx2.png rename to score-ios/Resources/Assets.xcassets/HighlightStar.imageset/HighlightStarx2.png diff --git a/score-ios/Resources/Assets.xcassets/Image.imageset/Contents.json b/score-ios/Resources/Assets.xcassets/Image.imageset/Contents.json new file mode 100644 index 0000000..a19a549 --- /dev/null +++ b/score-ios/Resources/Assets.xcassets/Image.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/score-ios/Views/DetailedViews/DetailedHighlightView.swift b/score-ios/Views/DetailedViews/DetailedHighlightView.swift index 91379c2..128c36c 100644 --- a/score-ios/Views/DetailedViews/DetailedHighlightView.swift +++ b/score-ios/Views/DetailedViews/DetailedHighlightView.swift @@ -30,6 +30,7 @@ struct DetailedHighlightsView: View { .resizable() .frame(width: 9.87, height: 18.57) } + Spacer() } } diff --git a/score-ios/Views/DetailedViews/NoHighlightView.swift b/score-ios/Views/DetailedViews/NoHighlightView.swift index d04d8be..9a6f418 100644 --- a/score-ios/Views/DetailedViews/NoHighlightView.swift +++ b/score-ios/Views/DetailedViews/NoHighlightView.swift @@ -10,7 +10,9 @@ import SwiftUI struct NoHighlightView: View { var body: some View { VStack { + Spacer() + VStack { Image("HighlightStar") .resizable() @@ -24,6 +26,7 @@ struct NoHighlightView: View { .font(Constants.Fonts.caption) .foregroundStyle(Constants.Colors.gray_text) } + Spacer() } } diff --git a/score-ios/Views/ListViews/HighlightTileArticle.swift b/score-ios/Views/ListViews/HighlightTileArticle.swift index 59870f3..16b7354 100644 --- a/score-ios/Views/ListViews/HighlightTileArticle.swift +++ b/score-ios/Views/ListViews/HighlightTileArticle.swift @@ -48,7 +48,6 @@ struct HighlightTileArticle: View { // Text overlay VStack(alignment: .leading, spacing: 0) { - // Title at top left Text(article.title) .font(.title3) .fontWeight(.bold) @@ -60,14 +59,7 @@ struct HighlightTileArticle: View { 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) @@ -90,7 +82,5 @@ struct HighlightTileArticle: View { ) } } - - } } diff --git a/score-ios/Views/ListViews/HighlightView.swift b/score-ios/Views/ListViews/HighlightView.swift index 5401fd1..27264fa 100644 --- a/score-ios/Views/ListViews/HighlightView.swift +++ b/score-ios/Views/ListViews/HighlightView.swift @@ -49,8 +49,6 @@ struct HighlightView: View { scope: .pastThreeDays ) } - - } } .environmentObject(viewModel) @@ -64,7 +62,6 @@ struct HighlightView: View { .onChange(of: viewModel.selectedSport) { _, _ in viewModel.filter() } - } } @@ -89,8 +86,7 @@ struct HighlightSectionView: View { VStack(alignment: .leading, spacing: 0) { NavigationLink(destination: DetailedHighlightsView(title: title, highlightScope: scope) - .environmentObject(viewModel)) - { + .environmentObject(viewModel)) { HStack { Text(title) .font(Constants.Fonts.subheader) From 7144d416b85a7812db0f89098e68d76a5da7dd7f Mon Sep 17 00:00:00 2001 From: Zain Bilal Date: Wed, 3 Dec 2025 21:32:44 -0500 Subject: [PATCH 12/12] Added youtubeVideo sport filter --- score-ios/Models/GraphQL/YoutubeVideo.graphql | 1 + score-ios/Models/GraphQL/schema.graphqls | 4 +++- score-ios/Models/YouTubeVideo.swift | 2 +- score-ios/ViewModels/HighlightsViewModel.swift | 2 -- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/score-ios/Models/GraphQL/YoutubeVideo.graphql b/score-ios/Models/GraphQL/YoutubeVideo.graphql index 6a9ce1a..e8d602b 100644 --- a/score-ios/Models/GraphQL/YoutubeVideo.graphql +++ b/score-ios/Models/GraphQL/YoutubeVideo.graphql @@ -8,5 +8,6 @@ query YoutubeVideos { url publishedAt duration + sportsType } } diff --git a/score-ios/Models/GraphQL/schema.graphqls b/score-ios/Models/GraphQL/schema.graphqls index 9bacad1..a02c9e1 100644 --- a/score-ios/Models/GraphQL/schema.graphqls +++ b/score-ios/Models/GraphQL/schema.graphqls @@ -74,6 +74,7 @@ Attributes: - url: The URL to the video. - published_at: The date and time the video was published. - duration: The duration of the video (optional). + - sportsType: The sport type extracted from the video title. """ type YoutubeVideoType { id: String @@ -84,6 +85,7 @@ type YoutubeVideoType { url: String! publishedAt: String! duration: String + sportsType: String } """ @@ -201,4 +203,4 @@ type CreateYoutubeVideo { type CreateArticle { article: ArticleType -} \ No newline at end of file +} diff --git a/score-ios/Models/YouTubeVideo.swift b/score-ios/Models/YouTubeVideo.swift index a4aec80..8d54361 100644 --- a/score-ios/Models/YouTubeVideo.swift +++ b/score-ios/Models/YouTubeVideo.swift @@ -37,7 +37,7 @@ struct YouTubeVideo: Identifiable { self.b64Thumbnail = gqlYouTubeVideo.b64Thumbnail self.url = gqlYouTubeVideo.url self.publishedAt = gqlYouTubeVideo.publishedAt - self.sport = .All + self.sport = Sport(normalizedValue: gqlYouTubeVideo.sportsType ?? "All") ?? .All self.duration = gqlYouTubeVideo.duration } } diff --git a/score-ios/ViewModels/HighlightsViewModel.swift b/score-ios/ViewModels/HighlightsViewModel.swift index 95cf272..73a51d3 100644 --- a/score-ios/ViewModels/HighlightsViewModel.swift +++ b/score-ios/ViewModels/HighlightsViewModel.swift @@ -77,8 +77,6 @@ class HighlightsViewModel: ObservableObject { self.allHighlights.sort(by: { $0.publishedAt > $1.publishedAt }) self.filter() - print(localArticles.map{$0.publishedAt}) - print(localYouTubeVideos.map{$0.publishedAt}) self.dataState = .success }