Files
Sportstime/SportsTime/Features/Home/Views/HomeView.swift
Trey t 9088b46563 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>
2026-01-07 00:46:40 -06:00

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)
}