Files
Sportstime/SportsTime/Features/Home/Views/SuggestedTripCard.swift
Trey t 415202e7f4 Add featured trips carousel on home screen
- Generate 8 suggested trips on app launch (2 per region: East, Central, West, Cross-Country)
- Each region has single-sport and multi-sport trip options
- Region classification based on stadium longitude
- Animated loading state with shimmer placeholders
- Loading messages use Foundation Models when available, fallback otherwise
- Tap card to view trip details in sheet
- Refresh button to regenerate trips
- Fixed-height cards with aligned top/bottom layout

New files:
- Region.swift: Geographic region enum with longitude classification
- LoadingTextGenerator.swift: On-device AI loading messages
- SuggestedTripsGenerator.swift: Trip generation service
- SuggestedTripCard.swift: Carousel card component
- LoadingTripsView.swift: Animated loading state

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 10:33:44 -06:00

148 lines
5.0 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 displayCities: [String]
if cities.count <= 3 {
displayCities = cities
} else {
displayCities = [cities.first ?? "", "...", cities.last ?? ""]
}
return VStack(alignment: .leading, spacing: 0) {
ForEach(Array(displayCities.enumerated()), id: \.offset) { index, city in
if index > 0 {
// Connector
HStack(spacing: 4) {
Text("|")
.font(.system(size: 10))
Image(systemName: "chevron.down")
.font(.system(size: 8))
}
.foregroundStyle(Theme.warmOrange.opacity(0.6))
.padding(.leading, 4)
}
Text(city)
.font(.system(size: Theme.FontSize.caption, weight: index == 0 ? .semibold : .regular))
.foregroundStyle(index == 0 ? Theme.textPrimary(colorScheme) : Theme.textSecondary(colorScheme))
.lineLimit(1)
}
}
}
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(stopNumber: 1, city: "New York", state: "NY", coordinate: nil, arrivalDate: Date(), departureDate: Date(), games: [], isRestDay: false),
TripStop(stopNumber: 2, city: "Boston", state: "MA", coordinate: nil, arrivalDate: Date(), departureDate: Date(), games: [], isRestDay: false),
TripStop(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()
}