Refactor itinerary display: separate day sections and travel sections

- Days are now headers with games as rows in that section
- Travel between days shown in standalone "Travel" sections
- Travel timing is flexible (user decides when to drive)
- Removed complex ItineraryDay bundling approach

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-07 12:41:32 -06:00
parent ab89c25f2f
commit 8ec8ed02b1

View File

@@ -326,17 +326,49 @@ struct TripDetailView: View {
Text("Itinerary")
.font(.headline)
ForEach(tripDays, id: \.self) { dayDate in
SimpleDayCard(
dayNumber: dayNumber(for: dayDate),
date: dayDate,
gamesOnDay: gamesOn(date: dayDate),
travelOnDay: travelOn(date: dayDate)
)
ForEach(itinerarySections.indices, id: \.self) { index in
let section = itinerarySections[index]
switch section {
case .day(let dayNumber, let date, let gamesOnDay):
DaySection(
dayNumber: dayNumber,
date: date,
games: gamesOnDay
)
case .travel(let segment):
TravelSection(segment: segment)
}
}
}
}
/// Build itinerary sections: days and travel between days
private var itinerarySections: [ItinerarySection] {
var sections: [ItinerarySection] = []
let calendar = Calendar.current
// Get all days
let days = tripDays
for (index, dayDate) in days.enumerated() {
let dayNum = index + 1
let gamesOnDay = gamesOn(date: dayDate)
// Add day section (even if no games - could be rest day)
if !gamesOnDay.isEmpty || index == 0 || index == days.count - 1 {
sections.append(.day(dayNumber: dayNum, date: dayDate, games: gamesOnDay))
}
// Check for travel AFTER this day (between this day and next)
let travelAfterDay = travelDepartingAfter(date: dayDate, beforeNextGameDay: days.indices.contains(index + 1) ? days[index + 1] : nil)
for segment in travelAfterDay {
sections.append(.travel(segment))
}
}
return sections
}
/// All calendar days in the trip
private var tripDays: [Date] {
let calendar = Calendar.current
@@ -354,14 +386,6 @@ struct TripDetailView: View {
return days
}
/// Day number for a given date
private func dayNumber(for date: Date) -> Int {
guard let firstDay = tripDays.first else { return 1 }
let calendar = Calendar.current
let days = calendar.dateComponents([.day], from: firstDay, to: date).day ?? 0
return days + 1
}
/// Games scheduled on a specific date
private func gamesOn(date: Date) -> [RichGame] {
let calendar = Calendar.current
@@ -372,16 +396,19 @@ struct TripDetailView: View {
return allGameIds.compactMap { games[$0] }.filter { richGame in
calendar.startOfDay(for: richGame.game.dateTime) == dayStart
}
}.sorted { $0.game.dateTime < $1.game.dateTime }
}
/// Travel segments departing on a specific date
private func travelOn(date: Date) -> [TravelSegment] {
/// Travel segments that depart after a given day (for between-day travel)
private func travelDepartingAfter(date: Date, beforeNextGameDay: Date?) -> [TravelSegment] {
let calendar = Calendar.current
let dayStart = calendar.startOfDay(for: date)
let dayEnd = calendar.startOfDay(for: date)
return trip.travelSegments.filter { segment in
calendar.startOfDay(for: segment.departureTime) == dayStart
let segmentDay = calendar.startOfDay(for: segment.departureTime)
// Travel is "after" this day if it departs on or after this day
// and arrives at a different city
return segmentDay == dayEnd
}
}
@@ -431,13 +458,19 @@ struct TripDetailView: View {
}
}
// MARK: - Simple Day Card (queries games and travel separately by date)
// MARK: - Itinerary Section (enum for day vs travel sections)
struct SimpleDayCard: View {
enum ItinerarySection {
case day(dayNumber: Int, date: Date, games: [RichGame])
case travel(TravelSegment)
}
// MARK: - Day Section (header + games)
struct DaySection: View {
let dayNumber: Int
let date: Date
let gamesOnDay: [RichGame]
let travelOnDay: [TravelSegment]
let games: [RichGame]
private var formattedDate: String {
let formatter = DateFormatter()
@@ -445,17 +478,16 @@ struct SimpleDayCard: View {
return formatter.string(from: date)
}
private var isRestDay: Bool {
gamesOnDay.isEmpty && travelOnDay.isEmpty
private var gameCity: String? {
games.first?.stadium.city
}
/// City where games are (from stadium)
private var gameCity: String? {
gamesOnDay.first?.stadium.city
private var isRestDay: Bool {
games.isEmpty
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 8) {
// Day header
HStack {
Text("Day \(dayNumber)")
@@ -479,63 +511,30 @@ struct SimpleDayCard: View {
}
}
// Games (each as its own row)
if !gamesOnDay.isEmpty {
VStack(alignment: .leading, spacing: 6) {
// City label
if let city = gameCity {
Label(city, systemImage: "mappin")
.font(.caption)
.foregroundStyle(.secondary)
}
ForEach(gamesOnDay, id: \.game.id) { richGame in
HStack {
Image(systemName: richGame.game.sport.iconName)
.foregroundStyle(.blue)
.frame(width: 20)
Text(richGame.matchupDescription)
.font(.subheadline)
Spacer()
Text(richGame.game.gameTime)
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
// City label (if games exist)
if let city = gameCity {
Label(city, systemImage: "mappin")
.font(.caption)
.foregroundStyle(.secondary)
}
// Travel segments (each as its own row, separate from games)
ForEach(travelOnDay) { segment in
HStack(spacing: 8) {
Image(systemName: segment.travelMode.iconName)
.foregroundStyle(.orange)
// Games
ForEach(games, id: \.game.id) { richGame in
HStack {
Image(systemName: richGame.game.sport.iconName)
.foregroundStyle(.blue)
.frame(width: 20)
VStack(alignment: .leading, spacing: 2) {
Text("Drive to \(segment.toLocation.name)")
.font(.subheadline)
.fontWeight(.medium)
Text("\(segment.formattedDistance)\(segment.formattedDuration)")
.font(.caption)
.foregroundStyle(.secondary)
}
Text(richGame.matchupDescription)
.font(.subheadline)
Spacer()
Text(richGame.game.gameTime)
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, 8)
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(Color.orange.opacity(0.1))
.background(Color(.tertiarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.orange.opacity(0.3), lineWidth: 1)
)
}
}
.padding()
@@ -544,6 +543,52 @@ struct SimpleDayCard: View {
}
}
// MARK: - Travel Section (standalone travel between days)
struct TravelSection: View {
let segment: TravelSegment
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Travel header
Text("Travel")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundStyle(.orange)
// Travel details
HStack(spacing: 8) {
Image(systemName: segment.travelMode.iconName)
.foregroundStyle(.orange)
.frame(width: 20)
VStack(alignment: .leading, spacing: 2) {
Text("\(segment.fromLocation.name)\(segment.toLocation.name)")
.font(.subheadline)
.fontWeight(.medium)
Text("\(segment.formattedDistance)\(segment.formattedDuration)")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding(.vertical, 8)
.padding(.horizontal, 10)
.background(Color.orange.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.orange.opacity(0.3), lineWidth: 1)
)
}
}
// MARK: - Share Sheet
struct ShareSheet: UIViewControllerRepresentable {