fix(display): use stadium timezone for all game time displays
Game times were incorrectly using device timezone in several views. Now all game time displays use the stadium's local timezone via RichGame.localGameTime/localGameTimeShort properties. Fixes: - PDFGenerator: was using game.gameDate (midnight UTC) instead of actual time - GameRowCompact: was formatting with device timezone - TimelineGameRow: was using .formatted() with device timezone Also adds Date+GameTime.swift extension for centralized timezone formatting. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
70
SportsTime/Core/Extensions/Date+GameTime.swift
Normal file
70
SportsTime/Core/Extensions/Date+GameTime.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user