Initial commit: SportsTime trip planning app

- Three-scenario planning engine (A: date range, B: selected games, C: directional routes)
- GeographicRouteExplorer with anchor game support for route exploration
- Shared ItineraryBuilder for travel segment calculation
- TravelEstimator for driving time/distance estimation
- SwiftUI views for trip creation and detail display
- CloudKit integration for schedule data
- Python scraping scripts for sports schedules

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-07 00:46:40 -06:00
commit 9088b46563
84 changed files with 180371 additions and 0 deletions

View File

@@ -0,0 +1,198 @@
//
// SavedTrip.swift
// SportsTime
//
// SwiftData models for local persistence
//
import Foundation
import SwiftData
@Model
final class SavedTrip {
@Attribute(.unique) var id: UUID
var name: String
var createdAt: Date
var updatedAt: Date
var status: String
var tripData: Data // Encoded Trip struct
@Relationship(deleteRule: .cascade)
var votes: [TripVote]?
init(
id: UUID = UUID(),
name: String,
createdAt: Date = Date(),
updatedAt: Date = Date(),
status: TripStatus = .planned,
tripData: Data
) {
self.id = id
self.name = name
self.createdAt = createdAt
self.updatedAt = updatedAt
self.status = status.rawValue
self.tripData = tripData
}
var trip: Trip? {
try? JSONDecoder().decode(Trip.self, from: tripData)
}
var tripStatus: TripStatus {
TripStatus(rawValue: status) ?? .draft
}
static func from(_ trip: Trip, status: TripStatus = .planned) -> SavedTrip? {
guard let data = try? JSONEncoder().encode(trip) else { return nil }
return SavedTrip(
id: trip.id,
name: trip.name,
createdAt: trip.createdAt,
updatedAt: trip.updatedAt,
status: status,
tripData: data
)
}
}
// MARK: - Trip Vote (Phase 2)
@Model
final class TripVote {
@Attribute(.unique) var id: UUID
var tripId: UUID
var voterId: String
var voterName: String
var gameVotes: Data // [UUID: Bool] encoded
var routeVotes: Data // [String: Int] encoded
var leisurePreference: String
var createdAt: Date
init(
id: UUID = UUID(),
tripId: UUID,
voterId: String,
voterName: String,
gameVotes: Data,
routeVotes: Data,
leisurePreference: LeisureLevel = .moderate,
createdAt: Date = Date()
) {
self.id = id
self.tripId = tripId
self.voterId = voterId
self.voterName = voterName
self.gameVotes = gameVotes
self.routeVotes = routeVotes
self.leisurePreference = leisurePreference.rawValue
self.createdAt = createdAt
}
}
// MARK: - User Preferences
@Model
final class UserPreferences {
@Attribute(.unique) var id: UUID
var defaultSports: Data // [Sport] encoded
var defaultTravelMode: String
var defaultLeisureLevel: String
var defaultLodgingType: String
var homeLocation: Data? // LocationInput encoded
var needsEVCharging: Bool
var numberOfDrivers: Int
var maxDrivingHours: Double?
init(
id: UUID = UUID(),
defaultSports: [Sport] = Sport.supported,
defaultTravelMode: TravelMode = .drive,
defaultLeisureLevel: LeisureLevel = .moderate,
defaultLodgingType: LodgingType = .hotel,
homeLocation: LocationInput? = nil,
needsEVCharging: Bool = false,
numberOfDrivers: Int = 1,
maxDrivingHours: Double? = nil
) {
self.id = id
self.defaultSports = (try? JSONEncoder().encode(defaultSports)) ?? Data()
self.defaultTravelMode = defaultTravelMode.rawValue
self.defaultLeisureLevel = defaultLeisureLevel.rawValue
self.defaultLodgingType = defaultLodgingType.rawValue
self.homeLocation = try? JSONEncoder().encode(homeLocation)
self.needsEVCharging = needsEVCharging
self.numberOfDrivers = numberOfDrivers
self.maxDrivingHours = maxDrivingHours
}
var sports: [Sport] {
(try? JSONDecoder().decode([Sport].self, from: defaultSports)) ?? Sport.supported
}
var travelMode: TravelMode {
TravelMode(rawValue: defaultTravelMode) ?? .drive
}
var leisureLevel: LeisureLevel {
LeisureLevel(rawValue: defaultLeisureLevel) ?? .moderate
}
var lodgingType: LodgingType {
LodgingType(rawValue: defaultLodgingType) ?? .hotel
}
var home: LocationInput? {
guard let data = homeLocation else { return nil }
return try? JSONDecoder().decode(LocationInput.self, from: data)
}
}
// MARK: - Cached Schedule
@Model
final class CachedSchedule {
@Attribute(.unique) var id: UUID
var sport: String
var season: String
var lastUpdated: Date
var gamesData: Data // [Game] encoded
var teamsData: Data // [Team] encoded
var stadiumsData: Data // [Stadium] encoded
init(
id: UUID = UUID(),
sport: Sport,
season: String,
lastUpdated: Date = Date(),
games: [Game],
teams: [Team],
stadiums: [Stadium]
) {
self.id = id
self.sport = sport.rawValue
self.season = season
self.lastUpdated = lastUpdated
self.gamesData = (try? JSONEncoder().encode(games)) ?? Data()
self.teamsData = (try? JSONEncoder().encode(teams)) ?? Data()
self.stadiumsData = (try? JSONEncoder().encode(stadiums)) ?? Data()
}
var games: [Game] {
(try? JSONDecoder().decode([Game].self, from: gamesData)) ?? []
}
var teams: [Team] {
(try? JSONDecoder().decode([Team].self, from: teamsData)) ?? []
}
var stadiums: [Stadium] {
(try? JSONDecoder().decode([Stadium].self, from: stadiumsData)) ?? []
}
var isStale: Bool {
let staleThreshold: TimeInterval = 24 * 60 * 60 // 24 hours
return Date().timeIntervalSince(lastUpdated) > staleThreshold
}
}