Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions score-ios.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
13E348FB2F3D212D0014EC63 /* CalendarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13E348FA2F3D21280014EC63 /* CalendarViewModel.swift */; };
1C87865D2D8CD76900EBDF74 /* TrailingFadeGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C87865C2D8CD76900EBDF74 /* TrailingFadeGradient.swift */; };
1C87865F2D8CDADC00EBDF74 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C87865E2D8CDADC00EBDF74 /* String+Extension.swift */; };
2384C7B81B22428D94240957 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840304A20FA141C291346BA8 /* Highlight.swift */; };
Expand Down Expand Up @@ -130,6 +131,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
13E348FA2F3D21280014EC63 /* CalendarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarViewModel.swift; sourceTree = "<group>"; };
1C87865C2D8CD76900EBDF74 /* TrailingFadeGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrailingFadeGradient.swift; sourceTree = "<group>"; };
1C87865E2D8CDADC00EBDF74 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = "<group>"; };
2C1375CA2E7233390089EBC7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -502,6 +504,7 @@
D87787C62CFFAE3D00EA79E1 /* ViewModels */ = {
isa = PBXGroup;
children = (
13E348FA2F3D21280014EC63 /* CalendarViewModel.swift */,
D87787C72CFFAE5200EA79E1 /* GamesViewModel.swift */,
7665A4062EB00528004A9903 /* HighlightsViewModel.swift */,
D864B5AA2D793A7400A3A50E /* PastGameViewModel.swift */,
Expand Down Expand Up @@ -743,6 +746,7 @@
FD5A38DB2D8F2BDD00CF5E30 /* GameLoadingView.swift in Sources */,
B136701ECD164EE9AC64667F /* Article.swift in Sources */,
64005CCECEAC4FD4BA8F51D2 /* YouTubeVideo.swift in Sources */,
13E348FB2F3D212D0014EC63 /* CalendarViewModel.swift in Sources */,
2384C7B81B22428D94240957 /* Highlight.swift in Sources */,
CE8ED4FC2D6BF47C00A274DE /* DummyData.swift in Sources */,
CE335CD92C9244230037F572 /* Game.swift in Sources */,
Expand Down Expand Up @@ -944,23 +948,26 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 26;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"score-ios/Preview Content\"";
DEVELOPMENT_TEAM = W7U2WA4D54;
DEVELOPMENT_TEAM = H5ZTDCQ89H;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "score-ios/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Score;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Allow calendar access to add Cornell games that aren't in your calendar.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UIUserInterfaceStyle = Light;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.3;
MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.cornellappdev.score-ios";
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
Expand All @@ -978,23 +985,26 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 26;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"score-ios/Preview Content\"";
DEVELOPMENT_TEAM = W7U2WA4D54;
DEVELOPMENT_TEAM = H5ZTDCQ89H;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "score-ios/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Score;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.sports";
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Allow calendar access to add Cornell games that aren't in your calendar.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UIUserInterfaceStyle = Light;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.3;
MARKETING_VERSION = 2.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.cornellappdev.score-ios";
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
Expand Down
8 changes: 2 additions & 6 deletions score-ios/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>SCORE_DEV_URL</key>
<string>$(SCORE_DEV_URL)</string>
<key>SCORE_PROD_URL</key>
Expand All @@ -19,5 +13,7 @@
<string>Poppins-Bold.ttf</string>
<string>Poppins-Regular.ttf</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
94 changes: 94 additions & 0 deletions score-ios/ViewModels/CalendarViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//
// CalendarViewModel.swift
// score-ios
//
// Created by Duru Alayli on 2/11/26.
//

import EventKit
import Foundation
import UIKit

final class CalendarViewModel: ObservableObject{
static let shared = CalendarViewModel()
private let eventStore = EKEventStore()
@Published var eachAlert: Alert?

private init() {}

struct Alert: Identifiable {
let id = UUID()
let alertTitle: String
let alertMessage: String
let openSettings: Bool
}

func requestAccessandAdd(event: Game) {
eventStore.requestFullAccessToEvents{ [weak self] (granted, error) in
guard let self = self else { return }
if granted && error == nil {

let title = "Cornell vs. \(event.opponent.name) \(event.sex) \(event.sport)"
let existing = self.eventStore.predicateForEvents(
withStart: event.date,
end: event.date.addingTimeInterval(7200),
calendars: [self.eventStore.defaultCalendarForNewEvents].compactMap { $0 }
)
let existingEvents = self.eventStore.events(matching: existing)

if existingEvents.contains(where: { $0.title == title && $0.startDate == event.date }) {
DispatchQueue.main.async {
self.showCalendarAlert (
alertTitle: "Game already added.",
alertMessage: "This game is already added to your calendar."
)
}
return
}

let calendarEvent = EKEvent(eventStore: self.eventStore)
calendarEvent.title = title
calendarEvent.startDate = event.date
calendarEvent.endDate = event.date.addingTimeInterval(7200)
calendarEvent.location = event.address
calendarEvent.calendar = self.eventStore.defaultCalendarForNewEvents

do {
try eventStore.save(calendarEvent, span: .thisEvent)
DispatchQueue.main.async {
if let url = URL(string: "calshow:\(event.date.timeIntervalSinceReferenceDate)") {
UIApplication.shared.open(url)
}
}
} catch {
DispatchQueue.main.async {
self.showCalendarAlert (
alertTitle: "Game can't be added.",
alertMessage: "There was an error adding Cornell vs. \(event.opponent.name) to your calendar."
)
}
}
} else {
DispatchQueue.main.async {
self.showCalendarAlert (
alertTitle: "Game can't be added.",
alertMessage: "Calendar access denied. Please enable full calendar access in Settings.",
openSettings: true
)
}
}
}
}

private func showCalendarAlert(
alertTitle: String,
alertMessage: String,
openSettings: Bool = false
) {
self.eachAlert = Alert(
alertTitle: alertTitle,
alertMessage: alertMessage,
openSettings: openSettings
)
}
}
9 changes: 4 additions & 5 deletions score-ios/ViewModels/GamesViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ class GamesViewModel: ObservableObject
// TODO: Remove once backend is has implemented pagination with sorted dates and pages by game type
func fetchGames() {
// Set loading state before fetch
dataState = (hasNotFetchedYet ? .loading : .refreshing)
dataState = (dataState == .success ? .refreshing : .loading)

self.privateUpcomingGames.removeAll()
self.privatePastGames.removeAll()
Expand Down Expand Up @@ -209,13 +209,12 @@ class GamesViewModel: ObservableObject
}

guard let fetchedGames = fetchedGames, !fetchedGames.isEmpty else {
// If this is the first fetch and no games, show empty data error
if offset == 0 {
// First page returned empty —> no games
self.dataState = .error(error: .emptyData)
} else {
// // Otherwise process all accumulated games
// self.processGames(accumulatedGames)
self.dataState = .error(error: .networkError)
// Process what we have
self.processGames(accumulatedGames)
}
return
}
Expand Down
2 changes: 1 addition & 1 deletion score-ios/ViewModels/HighlightsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class HighlightsViewModel: ObservableObject {

// MARK: - Loading
func loadHighlights() {
dataState = (hasNotFetchedYet ? .loading : .refreshing)
dataState = (dataState == .success ? .refreshing : .loading)

Task {
do {
Expand Down
110 changes: 64 additions & 46 deletions score-ios/Views/DetailedViews/GameView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import SwiftUI
struct GameView : View {
var game : Game
@ObservedObject var viewModel: PastGameViewModel
@ObservedObject var calendarViewModel = CalendarViewModel.shared
@State var viewState: Int = 0
@State var dayFromNow: Int = 0
@State var hourFromNow: Int = 0
Expand Down Expand Up @@ -119,14 +120,14 @@ extension GameView {
Text("\(game.sex.description) \(game.sport.description)")
.font(Constants.Fonts.subheader)
.foregroundStyle(Constants.Colors.black)

ScrollView(.horizontal, showsIndicators: false){
Text("Cornell vs. " + game.opponent.name.removingUniversityPrefix())
.font(Constants.Fonts.header)
.foregroundStyle(Constants.Colors.black)
}
.withTrailingFadeGradient()

HStack(spacing: 10) {
HStack {
Image("Location-g")
Expand Down Expand Up @@ -176,25 +177,50 @@ extension GameView {
.padding(.top, 8)
}
.padding(.top, 20)

// Ticketing Link Button
if let link = game.ticketLink,
let url = URL(string: link) {
HStack (spacing: 16){
// Ticketing Link Button
if let link = game.ticketLink,
let url = URL(string: link) {
Button(action: {
UIApplication.shared.open(url)
}) {
HStack (spacing: 9){
Image("Ticket")
.resizable()
.frame(width: 22, height: 22)
Text("Buy Tickets")
.foregroundStyle(Constants.Colors.white)
.font(.system(size: 16, weight: .medium))
.font(Constants.Fonts.buttonLabel)
}
.foregroundColor(.white)
.padding(12)
.background(
Constants.Colors.primary_red
)
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color.black.opacity(0.1), lineWidth: 1)
.shadow(color: Color.black.opacity(0.25), radius: 5, x: 0, y: 2)
)
.clipShape(RoundedRectangle(cornerRadius: 30))
}
}

// Calendar Button
Button(action: {
UIApplication.shared.open(url)
calendarViewModel.requestAccessandAdd(event:game)
}) {
HStack {
Image("Ticket")
HStack (spacing: 8){
Image("Calendar")
.resizable()
.frame(width: 25, height: 25)
Text("Buy Tickets")
.foregroundStyle(Constants.Colors.white)
.font(.system(size: 16, weight: .medium))
.frame(width: 24, height: 24)
Text("Add to Calendar")
.font(Constants.Fonts.buttonLabel)
.foregroundStyle(Constants.Colors.white)
}
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 15)
.padding(12)
.background(
Constants.Colors.primary_red
)
Expand All @@ -203,41 +229,33 @@ extension GameView {
.stroke(Color.black.opacity(0.1), lineWidth: 1)
.shadow(color: Color.black.opacity(0.25), radius: 5, x: 0, y: 2)
)
.clipShape(RoundedRectangle(cornerRadius: 30)) // Clip to shape to ensure rounded corners
.clipShape(RoundedRectangle(cornerRadius: 30))
}
.alert(item: $calendarViewModel.eachAlert) { alert in
if alert.openSettings {
return Alert (
title: Text(alert.alertTitle),
message: Text(alert.alertMessage),
primaryButton: .default(Text("Go to settings")) {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
},
secondaryButton: .cancel()
)
} else {
return Alert (
title: Text(alert.alertTitle),
message: Text(alert.alertMessage)
)
}
}
.padding(.top, 80)
}

// Calendar Button
// TODO: make this back when we have login
// Button(action: {
// // TODO: action
// }) {
// HStack {
// Image("Calendar")
// .resizable()
// .frame(width: 24, height: 24)
// Text("Add to Calendar")
// .font(Constants.Fonts.buttonLabel)
// .foregroundStyle(Constants.Colors.white)
// }
// .foregroundColor(.white)
// .padding(.horizontal, 16)
// .padding(.vertical, 10)
// .background(
// Constants.Colors.primary_red
// )
// .overlay(
// RoundedRectangle(cornerRadius: 30)
// .stroke(Color.black.opacity(0.1), lineWidth: 1)
// .shadow(color: Color.black.opacity(0.25), radius: 5, x: 0, y: 2)
// )
// .clipShape(RoundedRectangle(cornerRadius: 30)) // Clip to shape to ensure rounded corners
// }
// .padding(.top, 68)
// }
.padding(.top, 80)
}

}


private var summaryTab: some View {
NavigationLink(destination: ScoringSummary(game: game)) {
Expand Down
Loading