Files
Sportstime/SportsTime/Features/Home/Views/SuggestedTripCard.swift
Trey t 3e778473e6 Fix coast-to-coast trips and improve itinerary display
- Fix same-day different-city validation in C2C routes (no more impossible
  games like Detroit 7:30pm AND Milwaukee 8pm on the same day)
- Cap C2C trips at 14 days max with 3 middle stops, prefer shortest routes
- Add sport icon and name to game rows in trip itinerary
- Add horizontal scroll to route dots in suggested trip cards
- Allow swipe-to-dismiss on home sheet (trip planner still blocks)
- Generate travel segments for suggested trips
- Increase DAG route lookahead to 5 days for multi-day drives

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 11:42:27 -06:00

160 lines
5.6 KiB
Swift

//
// SuggestedTripCard.swift
// SportsTime
//
// Card component for displaying a suggested trip in the carousel.
//
import SwiftUI
struct SuggestedTripCard: View {
let suggestedTrip: SuggestedTrip
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Header: Region badge + Sport icons
HStack {
// Region badge
Text(suggestedTrip.region.shortName)
.font(.system(size: Theme.FontSize.micro, weight: .semibold))
.foregroundStyle(.white)
.padding(.horizontal, Theme.Spacing.xs)
.padding(.vertical, 4)
.background(regionColor)
.clipShape(Capsule())
Spacer()
// Sport icons
HStack(spacing: 4) {
ForEach(suggestedTrip.displaySports, id: \.self) { sport in
Image(systemName: sport.iconName)
.font(.system(size: 12))
.foregroundStyle(sport.themeColor)
}
}
}
// Route preview (vertical)
routePreview
Spacer()
// Stats row - inline compact display
HStack(spacing: 6) {
Label {
Text(suggestedTrip.trip.totalGames == 1 ? "1 game" : "\(suggestedTrip.trip.totalGames) games")
} icon: {
Image(systemName: "sportscourt")
}
Text("")
.foregroundStyle(Theme.textMuted(colorScheme).opacity(0.5))
Label {
Text(suggestedTrip.trip.stops.count == 1 ? "1 city" : "\(suggestedTrip.trip.stops.count) cities")
} icon: {
Image(systemName: "mappin")
}
}
.font(.system(size: 11))
.foregroundStyle(Theme.textSecondary(colorScheme))
// Date range
Text(suggestedTrip.trip.formattedDateRange)
.font(.system(size: Theme.FontSize.micro))
.foregroundStyle(Theme.textMuted(colorScheme))
}
.padding(Theme.Spacing.md)
.frame(width: 200, height: 160)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.shadow(color: Theme.cardShadow(colorScheme), radius: 8, y: 4)
}
private var routePreview: some View {
let cities = suggestedTrip.trip.stops.map { $0.city }
let startCity = cities.first ?? ""
let endCity = cities.last ?? ""
return VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
// Start End display
HStack(spacing: 6) {
Text(startCity)
.font(.system(size: Theme.FontSize.caption, weight: .semibold))
.foregroundStyle(Theme.textPrimary(colorScheme))
.lineLimit(1)
Image(systemName: "arrow.right")
.font(.system(size: 10, weight: .semibold))
.foregroundStyle(Theme.warmOrange)
Text(endCity)
.font(.system(size: Theme.FontSize.caption, weight: .semibold))
.foregroundStyle(Theme.textPrimary(colorScheme))
.lineLimit(1)
}
// Scrollable stop dots
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
ForEach(0..<cities.count, id: \.self) { index in
Circle()
.fill(index == 0 || index == cities.count - 1 ? Theme.warmOrange : Theme.routeGold.opacity(0.6))
.frame(width: 6, height: 6)
if index < cities.count - 1 {
Rectangle()
.fill(Theme.routeGold.opacity(0.4))
.frame(width: 8, height: 2)
}
}
}
.padding(.horizontal, Theme.Spacing.xs)
}
.frame(height: 12)
}
}
private var regionColor: Color {
switch suggestedTrip.region {
case .east: return .blue
case .central: return .green
case .west: return .orange
case .crossCountry: return .purple
}
}
}
#Preview {
let trip = Trip(
name: "Test Trip",
preferences: TripPreferences(),
stops: [
TripStop(id: UUID(), stopNumber: 1, city: "New York", state: "NY", coordinate: nil, arrivalDate: Date(), departureDate: Date(), games: [], isRestDay: false),
TripStop(id: UUID(),stopNumber: 2, city: "Boston", state: "MA", coordinate: nil, arrivalDate: Date(), departureDate: Date(), games: [], isRestDay: false),
TripStop(id: UUID(),stopNumber: 3, city: "Philadelphia", state: "PA", coordinate: nil, arrivalDate: Date(), departureDate: Date(), games: [], isRestDay: false)
],
totalGames: 5
)
let suggestedTrip = SuggestedTrip(
id: UUID(),
region: .east,
isSingleSport: false,
trip: trip,
richGames: [:],
sports: [.mlb, .nba]
)
SuggestedTripCard(suggestedTrip: suggestedTrip)
.padding()
}