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:
Trey t
2026-01-20 22:44:40 -06:00
parent 74fd21590b
commit 166ad5d6f9
4 changed files with 75 additions and 9 deletions

View 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)
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)
}