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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user