Redesign trip option cards and fix various UI/planning issues
TripOptionCard improvements: - Replace horizontal route with vertical layout (start → end with arrow) - Remove rank badges (1, 2, 3, etc.) - Split stats into two rows: cities/miles and sports with game counts - Clear selection when navigating back from detail view Settings cleanup: - Remove unused settings (preferred game time, playoff games, notifications) - Convert remaining settings to sliders Planning fixes: - Fix multi-day driving calculation in canTransition - Remove over-restrictive trip rejection in TravelEstimator - Clear games cache when sport selection changes UI polish: - RoutePreviewStrip shows all cities (abbreviated) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1106,10 +1106,9 @@ struct TripOptionsView: View {
|
||||
.padding(.bottom, Theme.Spacing.sm)
|
||||
|
||||
// Options list
|
||||
ForEach(Array(sortedOptions.enumerated()), id: \.element.id) { index, option in
|
||||
ForEach(sortedOptions) { option in
|
||||
TripOptionCard(
|
||||
option: option,
|
||||
rank: index + 1,
|
||||
games: games,
|
||||
onSelect: {
|
||||
selectedTrip = convertToTrip(option)
|
||||
@@ -1129,6 +1128,11 @@ struct TripOptionsView: View {
|
||||
TripDetailView(trip: trip, games: games)
|
||||
}
|
||||
}
|
||||
.onChange(of: showTripDetail) { _, isShowing in
|
||||
if !isShowing {
|
||||
selectedTrip = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var sortPicker: some View {
|
||||
@@ -1168,7 +1172,6 @@ struct TripOptionsView: View {
|
||||
|
||||
struct TripOptionCard: View {
|
||||
let option: ItineraryOption
|
||||
let rank: Int
|
||||
let games: [UUID: RichGame]
|
||||
let onSelect: () -> Void
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@@ -1184,34 +1187,50 @@ struct TripOptionCard: View {
|
||||
option.stops.flatMap { $0.games }.count
|
||||
}
|
||||
|
||||
private var routeDescription: String {
|
||||
if uniqueCities.count <= 2 {
|
||||
return uniqueCities.joined(separator: " → ")
|
||||
private var uniqueSports: [Sport] {
|
||||
let gameIds = option.stops.flatMap { $0.games }
|
||||
let sports = gameIds.compactMap { games[$0]?.game.sport }
|
||||
return Array(Set(sports)).sorted { $0.rawValue < $1.rawValue }
|
||||
}
|
||||
|
||||
private var gamesPerSport: [(sport: Sport, count: Int)] {
|
||||
let gameIds = option.stops.flatMap { $0.games }
|
||||
var countsBySport: [Sport: Int] = [:]
|
||||
for gameId in gameIds {
|
||||
if let sport = games[gameId]?.game.sport {
|
||||
countsBySport[sport, default: 0] += 1
|
||||
}
|
||||
}
|
||||
return "\(uniqueCities.first ?? "") → \(uniqueCities.count - 2) stops → \(uniqueCities.last ?? "")"
|
||||
return countsBySport.sorted { $0.key.rawValue < $1.key.rawValue }
|
||||
.map { (sport: $0.key, count: $0.value) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: onSelect) {
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
// Left: Rank badge
|
||||
Text("\(rank)")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 36, height: 36)
|
||||
.background(rank == 1 ? Theme.warmOrange : Theme.textMuted(colorScheme))
|
||||
.clipShape(Circle())
|
||||
|
||||
// Middle: Route info
|
||||
// Route info
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(routeDescription)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
.lineLimit(1)
|
||||
// Vertical route display
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(uniqueCities.first ?? "")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
|
||||
// Stats row
|
||||
VStack(spacing: 0) {
|
||||
Text("|")
|
||||
.font(.system(size: 10))
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.system(size: 8, weight: .bold))
|
||||
}
|
||||
.foregroundStyle(Theme.warmOrange)
|
||||
|
||||
Text(uniqueCities.last ?? "")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.foregroundStyle(Theme.textPrimary(colorScheme))
|
||||
}
|
||||
|
||||
// Top stats row: cities and miles
|
||||
HStack(spacing: 12) {
|
||||
Label("\(totalGames) games", systemImage: "sportscourt")
|
||||
Label("\(uniqueCities.count) cities", systemImage: "mappin")
|
||||
if option.totalDistanceMiles > 0 {
|
||||
Label("\(Int(option.totalDistanceMiles)) mi", systemImage: "car")
|
||||
@@ -1220,6 +1239,23 @@ struct TripOptionCard: View {
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
// Bottom row: sports with game counts
|
||||
HStack(spacing: 6) {
|
||||
ForEach(gamesPerSport, id: \.sport) { item in
|
||||
HStack(spacing: 3) {
|
||||
Image(systemName: item.sport.iconName)
|
||||
.font(.system(size: 9))
|
||||
Text("\(item.sport.rawValue.uppercased()) \(item.count)")
|
||||
.font(.system(size: 9, weight: .semibold))
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 3)
|
||||
.background(item.sport.themeColor.opacity(0.15))
|
||||
.foregroundStyle(item.sport.themeColor)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
|
||||
// AI-generated description (after stats)
|
||||
if let description = aiDescription {
|
||||
Text(description)
|
||||
@@ -1250,7 +1286,7 @@ struct TripOptionCard: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.stroke(rank == 1 ? Theme.warmOrange : Theme.surfaceGlow(colorScheme), lineWidth: rank == 1 ? 2 : 1)
|
||||
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Reference in New Issue
Block a user