- 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>
307 lines
8.8 KiB
Swift
307 lines
8.8 KiB
Swift
//
|
|
// HomeView.swift
|
|
// SportsTime
|
|
//
|
|
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
struct HomeView: View {
|
|
@Environment(\.modelContext) private var modelContext
|
|
@Query(sort: \SavedTrip.updatedAt, order: .reverse) private var savedTrips: [SavedTrip]
|
|
|
|
@State private var showNewTrip = false
|
|
@State private var selectedTab = 0
|
|
|
|
var body: some View {
|
|
TabView(selection: $selectedTab) {
|
|
// Home Tab
|
|
NavigationStack {
|
|
ScrollView {
|
|
VStack(spacing: 24) {
|
|
// Hero Card
|
|
heroCard
|
|
|
|
// Quick Actions
|
|
quickActions
|
|
|
|
// Saved Trips
|
|
if !savedTrips.isEmpty {
|
|
savedTripsSection
|
|
}
|
|
|
|
// Featured / Tips
|
|
tipsSection
|
|
}
|
|
.padding()
|
|
}
|
|
.navigationTitle("Sport Travel Planner")
|
|
.toolbar {
|
|
ToolbarItem(placement: .primaryAction) {
|
|
Button {
|
|
showNewTrip = true
|
|
} label: {
|
|
Image(systemName: "plus")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.tabItem {
|
|
Label("Home", systemImage: "house.fill")
|
|
}
|
|
.tag(0)
|
|
|
|
// Schedule Tab
|
|
NavigationStack {
|
|
ScheduleListView()
|
|
}
|
|
.tabItem {
|
|
Label("Schedule", systemImage: "calendar")
|
|
}
|
|
.tag(1)
|
|
|
|
// My Trips Tab
|
|
NavigationStack {
|
|
SavedTripsListView(trips: savedTrips)
|
|
}
|
|
.tabItem {
|
|
Label("My Trips", systemImage: "suitcase.fill")
|
|
}
|
|
.tag(2)
|
|
|
|
// Settings Tab
|
|
NavigationStack {
|
|
SettingsView()
|
|
}
|
|
.tabItem {
|
|
Label("Settings", systemImage: "gear")
|
|
}
|
|
.tag(3)
|
|
}
|
|
.sheet(isPresented: $showNewTrip) {
|
|
TripCreationView()
|
|
}
|
|
}
|
|
|
|
// MARK: - Hero Card
|
|
|
|
private var heroCard: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Plan Your Ultimate Sports Road Trip")
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
|
|
Text("Visit multiple stadiums, catch live games, and create unforgettable memories.")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
|
|
Button {
|
|
showNewTrip = true
|
|
} label: {
|
|
Text("Start Planning")
|
|
.fontWeight(.semibold)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(Color.blue)
|
|
.foregroundStyle(.white)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
}
|
|
.padding()
|
|
.background(
|
|
LinearGradient(
|
|
colors: [.blue.opacity(0.1), .green.opacity(0.1)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
.clipShape(RoundedRectangle(cornerRadius: 16))
|
|
}
|
|
|
|
// MARK: - Quick Actions
|
|
|
|
private var quickActions: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Quick Start")
|
|
.font(.headline)
|
|
|
|
HStack(spacing: 12) {
|
|
ForEach(Sport.supported) { sport in
|
|
QuickSportButton(sport: sport) {
|
|
// Start trip with this sport pre-selected
|
|
showNewTrip = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Saved Trips
|
|
|
|
private var savedTripsSection: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
Text("Recent Trips")
|
|
.font(.headline)
|
|
Spacer()
|
|
Button("See All") {
|
|
selectedTab = 2
|
|
}
|
|
.font(.subheadline)
|
|
}
|
|
|
|
ForEach(savedTrips.prefix(3)) { savedTrip in
|
|
if let trip = savedTrip.trip {
|
|
SavedTripCard(trip: trip)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Tips
|
|
|
|
private var tipsSection: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Planning Tips")
|
|
.font(.headline)
|
|
|
|
VStack(spacing: 8) {
|
|
TipRow(icon: "calendar.badge.clock", title: "Check schedules early", subtitle: "Game times can change, sync often")
|
|
TipRow(icon: "car.fill", title: "Plan rest days", subtitle: "Don't overdo the driving")
|
|
TipRow(icon: "star.fill", title: "Mark must-sees", subtitle: "Ensure your favorite matchups are included")
|
|
}
|
|
.padding()
|
|
.background(Color(.secondarySystemBackground))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Supporting Views
|
|
|
|
struct QuickSportButton: View {
|
|
let sport: Sport
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
VStack(spacing: 8) {
|
|
Image(systemName: sport.iconName)
|
|
.font(.title)
|
|
Text(sport.rawValue)
|
|
.font(.caption)
|
|
.fontWeight(.medium)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 16)
|
|
.background(Color(.secondarySystemBackground))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
struct SavedTripCard: View {
|
|
let trip: Trip
|
|
|
|
var body: some View {
|
|
NavigationLink {
|
|
TripDetailView(trip: trip, games: [:])
|
|
} label: {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(trip.name)
|
|
.font(.subheadline)
|
|
.fontWeight(.semibold)
|
|
|
|
Text(trip.formattedDateRange)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
|
|
HStack(spacing: 8) {
|
|
Label("\(trip.stops.count) cities", systemImage: "mappin")
|
|
Label("\(trip.totalGames) games", systemImage: "sportscourt")
|
|
}
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "chevron.right")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding()
|
|
.background(Color(.secondarySystemBackground))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
struct TipRow: View {
|
|
let icon: String
|
|
let title: String
|
|
let subtitle: String
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: icon)
|
|
.font(.title3)
|
|
.foregroundStyle(.blue)
|
|
.frame(width: 30)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(title)
|
|
.font(.subheadline)
|
|
.fontWeight(.medium)
|
|
Text(subtitle)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Saved Trips List View
|
|
|
|
struct SavedTripsListView: View {
|
|
let trips: [SavedTrip]
|
|
|
|
var body: some View {
|
|
List {
|
|
if trips.isEmpty {
|
|
ContentUnavailableView(
|
|
"No Saved Trips",
|
|
systemImage: "suitcase",
|
|
description: Text("Your planned trips will appear here")
|
|
)
|
|
} else {
|
|
ForEach(trips) { savedTrip in
|
|
if let trip = savedTrip.trip {
|
|
NavigationLink {
|
|
TripDetailView(trip: trip, games: [:])
|
|
} label: {
|
|
VStack(alignment: .leading) {
|
|
Text(trip.name)
|
|
.font(.headline)
|
|
Text(trip.formattedDateRange)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("My Trips")
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
HomeView()
|
|
.modelContainer(for: SavedTrip.self, inMemory: true)
|
|
}
|