diff --git a/SportsTime/Core/Extensions/Date+GameTime.swift b/SportsTime/Core/Extensions/Date+GameTime.swift new file mode 100644 index 0000000..bbfb15c --- /dev/null +++ b/SportsTime/Core/Extensions/Date+GameTime.swift @@ -0,0 +1,70 @@ +// +// Date+GameTime.swift +// SportsTime +// +// Centralized game time formatting with timezone support. +// + +import Foundation + +extension Date { + + /// Formats the date as a game time string in the specified timezone. + /// + /// - Parameters: + /// - timeZone: The timezone to display the time in (typically the stadium's timezone). + /// Falls back to the device's current timezone if nil. + /// - includeZone: Whether to include the timezone abbreviation (e.g., "EDT", "PST"). + /// - Returns: A formatted time string like "7:00 PM" or "7:00 PM EDT". + /// + /// Usage: + /// ```swift + /// // Without timezone indicator (for UI cards) + /// game.dateTime.gameTimeString(in: stadium.timeZone) // "7:00 PM" + /// + /// // With timezone indicator (for exports, detail views) + /// game.dateTime.gameTimeString(in: stadium.timeZone, includeZone: true) // "7:00 PM EDT" + /// ``` + func gameTimeString(in timeZone: TimeZone?, includeZone: Bool = false) -> String { + let formatter = DateFormatter() + formatter.dateFormat = includeZone ? "h:mm a z" : "h:mm a" + formatter.timeZone = timeZone ?? .current + return formatter.string(from: self) + } + + /// Formats the date as a full game date and time string in the specified timezone. + /// + /// - Parameters: + /// - timeZone: The timezone to display in (typically the stadium's timezone). + /// - includeZone: Whether to include the timezone abbreviation. + /// - Returns: A formatted string like "Sat, Jan 18 at 7:00 PM" or "Sat, Jan 18 at 7:00 PM EDT". + func gameDateTimeString(in timeZone: TimeZone?, includeZone: Bool = false) -> String { + let formatter = DateFormatter() + formatter.dateFormat = includeZone ? "EEE, MMM d 'at' h:mm a z" : "EEE, MMM d 'at' h:mm a" + formatter.timeZone = timeZone ?? .current + return formatter.string(from: self) + } + + /// Returns the start of day in the specified timezone. + /// + /// This is useful for grouping games by calendar day in the stadium's local timezone, + /// not the user's device timezone. + /// + /// - Parameter timeZone: The timezone to calculate start of day in. + /// - Returns: A Date representing midnight in the specified timezone. + func startOfDay(in timeZone: TimeZone?) -> Date { + var calendar = Calendar.current + calendar.timeZone = timeZone ?? .current + return calendar.startOfDay(for: self) + } + + /// Returns the calendar day components in the specified timezone. + /// + /// - Parameter timeZone: The timezone to extract components in. + /// - Returns: DateComponents with year, month, and day. + func calendarDay(in timeZone: TimeZone?) -> DateComponents { + var calendar = Calendar.current + calendar.timeZone = timeZone ?? .current + return calendar.dateComponents([.year, .month, .day], from: self) + } +} diff --git a/SportsTime/Export/PDFGenerator.swift b/SportsTime/Export/PDFGenerator.swift index 5845845..b5e0670 100644 --- a/SportsTime/Export/PDFGenerator.swift +++ b/SportsTime/Export/PDFGenerator.swift @@ -562,10 +562,8 @@ final class PDFGenerator { ] (teamsText as NSString).draw(at: CGPoint(x: margin + 14, y: currentY), withAttributes: teamsAttributes) - // Time: "7:00 PM" - let timeFormatter = DateFormatter() - timeFormatter.dateFormat = "h:mm a" // "7:00 PM" - let timeText = timeFormatter.string(from: richGame.game.gameDate) + // Time: "7:00 PM EDT" (stadium local time with timezone indicator) + let timeText = richGame.localGameTime let timeAttributes: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 13), .foregroundColor: textSecondary diff --git a/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift b/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift index 02eaa9c..1a1c67f 100644 --- a/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift +++ b/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift @@ -1118,9 +1118,7 @@ struct GameRowCompact: View { let colorScheme: ColorScheme private var formattedTime: String { - let formatter = DateFormatter() - formatter.dateFormat = "h:mm a" // "8:00 PM" - return formatter.string(from: richGame.game.dateTime) + richGame.localGameTimeShort // Stadium local time } var body: some View { diff --git a/SportsTime/Features/Trip/Views/TimelineItemView.swift b/SportsTime/Features/Trip/Views/TimelineItemView.swift index d0dd9a8..3df3368 100644 --- a/SportsTime/Features/Trip/Views/TimelineItemView.swift +++ b/SportsTime/Features/Trip/Views/TimelineItemView.swift @@ -270,9 +270,9 @@ struct TimelineGameRow: View { .font(.subheadline) .fontWeight(.medium) - // Time and venue + // Time and venue (stadium local time) HStack(spacing: 4) { - Text(richGame.game.dateTime.formatted(date: .omitted, time: .shortened)) + Text(richGame.localGameTimeShort) Text("•") Text(richGame.stadium.name) }