- Change attraction category color from .yellow to orange variant for readable text in both light and dark mode (QuickAddItemSheet, POIDetailSheet) - Fix "optional" badge to use textPrimary-based colors for strong contrast - Bump paywall dashed separator from 0.4 to 0.6 opacity - Fix SuggestedTripCard bullet separator from textMuted(0.5) to textSecondary Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
169 lines
5.8 KiB
Swift
169 lines
5.8 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(.caption)
|
|
.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(.caption)
|
|
.foregroundStyle(sport.themeColor)
|
|
.accessibilityHidden(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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.textSecondary(colorScheme))
|
|
|
|
Label {
|
|
Text(suggestedTrip.trip.stops.count == 1 ? "1 city" : "\(suggestedTrip.trip.stops.count) cities")
|
|
} icon: {
|
|
Image(systemName: "mappin")
|
|
}
|
|
}
|
|
.font(.caption2)
|
|
.foregroundStyle(Theme.textSecondary(colorScheme))
|
|
|
|
// Date range
|
|
Text(suggestedTrip.trip.formattedDateRange)
|
|
.font(.caption)
|
|
.foregroundStyle(Theme.textMuted(colorScheme))
|
|
}
|
|
.padding(Theme.Spacing.md)
|
|
.padding(.bottom, Theme.Spacing.xs)
|
|
.frame(width: 200, height: 175)
|
|
.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)
|
|
.accessibilityElement(children: .combine)
|
|
}
|
|
|
|
private var routePreview: some View {
|
|
let cities = suggestedTrip.trip.stops.map { $0.city }
|
|
let startCity = cities.first ?? ""
|
|
let endCity = cities.last ?? ""
|
|
let routeDescription = cities.joined(separator: " to ")
|
|
|
|
return VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
|
|
// Start → End display
|
|
HStack(spacing: 6) {
|
|
Text(startCity)
|
|
.font(.subheadline)
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
.lineLimit(1)
|
|
|
|
Image(systemName: "arrow.right")
|
|
.font(.caption2)
|
|
.foregroundStyle(Theme.warmOrange)
|
|
.accessibilityHidden(true)
|
|
|
|
Text(endCity)
|
|
.font(.subheadline)
|
|
.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)
|
|
.accessibilityHidden(true)
|
|
|
|
if index < cities.count - 1 {
|
|
Rectangle()
|
|
.fill(Theme.routeGold.opacity(0.4))
|
|
.frame(width: 8, height: 2)
|
|
.accessibilityHidden(true)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, Theme.Spacing.xs)
|
|
}
|
|
.frame(height: 12)
|
|
}
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityLabel("Route: \(routeDescription)")
|
|
}
|
|
|
|
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()
|
|
}
|