Files
Sportstime/SportsTime/Features/Home/Views/SuggestedTripCard.swift
Trey t 7efcea7bd4 Add canonical ID pipeline and fix UUID consistency for CloudKit sync
- Add local canonicalization pipeline (stadiums, teams, games) that generates
  deterministic canonical IDs before CloudKit upload
- Fix CanonicalSyncService to use deterministic UUIDs from canonical IDs
  instead of random UUIDs from CloudKit records
- Add SyncStadium/SyncTeam/SyncGame types to CloudKitService that preserve
  canonical ID relationships during sync
- Add canonical ID field keys to CKModels for reading from CloudKit records
- Bundle canonical JSON files (stadiums_canonical, teams_canonical,
  games_canonical, stadium_aliases) for consistent bootstrap data
- Update BootstrapService to prefer canonical format files over legacy format

This ensures all entities use consistent deterministic UUIDs derived from
their canonical IDs, preventing duplicate records when syncing CloudKit
data with bootstrapped local data.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 10:30:09 -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(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()
}