// // HomeView.swift // SportsTime // import SwiftUI import SwiftData struct HomeView: View { @Environment(\.modelContext) private var modelContext @Environment(\.colorScheme) private var colorScheme @Query(sort: \SavedTrip.updatedAt, order: .reverse) private var savedTrips: [SavedTrip] @State private var showNewTrip = false @State private var selectedSport: Sport? @State private var selectedTab = 0 @State private var suggestedTripsGenerator = SuggestedTripsGenerator() @State private var selectedSuggestedTrip: SuggestedTrip? @State private var displayedTips: [PlanningTip] = [] @State private var showProPaywall = false var body: some View { TabView(selection: $selectedTab) { // Home Tab NavigationStack { AdaptiveHomeContent( showNewTrip: $showNewTrip, selectedTab: $selectedTab, selectedSuggestedTrip: $selectedSuggestedTrip, savedTrips: savedTrips, suggestedTripsGenerator: suggestedTripsGenerator, displayedTips: displayedTips ) .toolbar { ToolbarItem(placement: .primaryAction) { Button { showNewTrip = true } label: { Image(systemName: "plus.circle.fill") .font(.title2) .foregroundStyle(Theme.warmOrange) } } } } .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) // Progress Tab NavigationStack { if StoreManager.shared.isPro { ProgressTabView() } else { ProLockedView(feature: .progressTracking) { showProPaywall = true } } } .tabItem { Label("Progress", systemImage: "chart.bar.fill") } .tag(3) // Settings Tab NavigationStack { SettingsView() } .tabItem { Label("Settings", systemImage: "gear") } .tag(4) } .tint(Theme.warmOrange) .sheet(isPresented: $showNewTrip) { TripWizardView() } .onChange(of: showNewTrip) { _, isShowing in if !isShowing { selectedSport = nil } } .task { if suggestedTripsGenerator.suggestedTrips.isEmpty && !suggestedTripsGenerator.isLoading { await suggestedTripsGenerator.generateTrips() } } .sheet(item: $selectedSuggestedTrip) { suggestedTrip in NavigationStack { TripDetailView(trip: suggestedTrip.trip, games: suggestedTrip.richGames) } } .sheet(isPresented: $showProPaywall) { PaywallView() } } // MARK: - Hero Card private var heroCard: some View { VStack(spacing: Theme.Spacing.lg) { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { Text("Adventure Awaits") .font(.largeTitle) .foregroundStyle(Theme.textPrimary(colorScheme)) Text("Plan your ultimate sports road trip. Visit stadiums, catch games, and create unforgettable memories.") .font(.body) .foregroundStyle(Theme.textSecondary(colorScheme)) .fixedSize(horizontal: false, vertical: true) } .frame(maxWidth: .infinity, alignment: .leading) Button { showNewTrip = true } label: { HStack(spacing: Theme.Spacing.xs) { Image(systemName: "map.fill") Text("Start Planning") .fontWeight(.semibold) } .frame(maxWidth: .infinity) .padding(Theme.Spacing.md) .background(Theme.warmOrange) .foregroundStyle(.white) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) } .pressableStyle() .glowEffect(color: Theme.warmOrange, radius: 12) } .padding(Theme.Spacing.lg) .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.xlarge)) .overlay { RoundedRectangle(cornerRadius: Theme.CornerRadius.xlarge) .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) } .shadow(color: Theme.cardShadow(colorScheme), radius: 15, y: 8) } // MARK: - Quick Actions private var quickActions: some View { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { Text("Quick Start") .font(.title2) .foregroundStyle(Theme.textPrimary(colorScheme)) SportSelectorGrid { sport in selectedSport = sport showNewTrip = true } .padding(.horizontal, Theme.Spacing.md) .padding(.vertical, Theme.Spacing.md) .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) .overlay { RoundedRectangle(cornerRadius: Theme.CornerRadius.large) .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) } } } // MARK: - Suggested Trips @ViewBuilder private var suggestedTripsSection: some View { if suggestedTripsGenerator.isLoading { LoadingTripsView(message: suggestedTripsGenerator.loadingMessage) } else if !suggestedTripsGenerator.suggestedTrips.isEmpty { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { // Header with refresh button HStack { Text("Featured Trips") .font(.title2) .foregroundStyle(Theme.textPrimary(colorScheme)) Spacer() Button { Task { await suggestedTripsGenerator.refreshTrips() } } label: { Image(systemName: "arrow.clockwise") .font(.subheadline) .foregroundStyle(Theme.warmOrange) } } // Horizontal carousel grouped by region ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: Theme.Spacing.lg) { ForEach(suggestedTripsGenerator.tripsByRegion, id: \.region) { regionGroup in VStack(alignment: .leading, spacing: Theme.Spacing.sm) { // Region header HStack(spacing: Theme.Spacing.xs) { Image(systemName: regionGroup.region.iconName) .font(.caption) Text(regionGroup.region.shortName) .font(.subheadline) } .foregroundStyle(Theme.textSecondary(colorScheme)) // Trip cards for this region HStack(spacing: Theme.Spacing.md) { ForEach(regionGroup.trips) { suggestedTrip in Button { selectedSuggestedTrip = suggestedTrip } label: { SuggestedTripCard(suggestedTrip: suggestedTrip) } .buttonStyle(.plain) } } } } } .padding(.horizontal, 1) // Prevent clipping } } } else if let error = suggestedTripsGenerator.error { // Error state VStack(alignment: .leading, spacing: Theme.Spacing.sm) { Text("Featured Trips") .font(.title2) .foregroundStyle(Theme.textPrimary(colorScheme)) HStack { Image(systemName: "exclamationmark.triangle") .foregroundStyle(.orange) Text(error) .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) Spacer() Button("Retry") { Task { await suggestedTripsGenerator.generateTrips() } } .font(.subheadline) .foregroundStyle(Theme.warmOrange) } .padding(Theme.Spacing.md) .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) } } } // MARK: - Saved Trips private var savedTripsSection: some View { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { HStack { Text("Recent Trips") .font(.title2) .foregroundStyle(Theme.textPrimary(colorScheme)) Spacer() Button { selectedTab = 2 } label: { HStack(spacing: 4) { Text("See All") Image(systemName: "chevron.right") .font(.caption) } .font(.subheadline) .foregroundStyle(Theme.warmOrange) } } ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in if let trip = savedTrip.trip { SavedTripCard(savedTrip: savedTrip, trip: trip) .staggeredAnimation(index: index, delay: 0.05) } } } } // MARK: - Tips private var tipsSection: some View { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { Text("Planning Tips") .font(.title2) .foregroundStyle(Theme.textPrimary(colorScheme)) VStack(spacing: Theme.Spacing.xs) { ForEach(displayedTips) { tip in TipRow(icon: tip.icon, title: tip.title, subtitle: tip.subtitle) } } .padding(Theme.Spacing.md) .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) .overlay { RoundedRectangle(cornerRadius: Theme.CornerRadius.large) .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) } } .onAppear { if displayedTips.isEmpty { displayedTips = PlanningTips.random(3) } } } } // MARK: - Supporting Views struct SavedTripCard: View { let savedTrip: SavedTrip let trip: Trip @Environment(\.colorScheme) private var colorScheme var body: some View { NavigationLink { TripDetailView(trip: trip, games: savedTrip.games) } label: { HStack(spacing: Theme.Spacing.md) { // Route preview icon ZStack { Circle() .fill(Theme.warmOrange.opacity(0.15)) .frame(width: 44, height: 44) Image(systemName: "map.fill") .foregroundStyle(Theme.warmOrange) } VStack(alignment: .leading, spacing: 4) { Text(trip.name) .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) Text(trip.formattedDateRange) .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) HStack(spacing: Theme.Spacing.sm) { HStack(spacing: 4) { Image(systemName: "mappin") .font(.caption2) Text("\(trip.stops.count) cities") } HStack(spacing: 4) { Image(systemName: "sportscourt") .font(.caption2) Text("\(trip.totalGames) games") } } .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) } Spacer() Image(systemName: "chevron.right") .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) } .padding(Theme.Spacing.md) .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) .overlay { RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) } } .buttonStyle(.plain) } } struct TipRow: View { let icon: String let title: String let subtitle: String @Environment(\.colorScheme) private var colorScheme var body: some View { HStack(spacing: Theme.Spacing.sm) { ZStack { Circle() .fill(Theme.routeGold.opacity(0.15)) .frame(width: 36, height: 36) Image(systemName: icon) .font(.subheadline) .foregroundStyle(Theme.routeGold) } VStack(alignment: .leading, spacing: 2) { Text(title) .font(.subheadline) .foregroundStyle(Theme.textPrimary(colorScheme)) Text(subtitle) .font(.caption) .foregroundStyle(Theme.textSecondary(colorScheme)) } Spacer() } } } // MARK: - Saved Trips List View struct SavedTripsListView: View { let trips: [SavedTrip] @Environment(\.colorScheme) private var colorScheme @State private var polls: [TripPoll] = [] @State private var isLoadingPolls = false @State private var showCreatePoll = false @State private var selectedPoll: TripPoll? /// Trips sorted by most cities (stops) first private var sortedTrips: [SavedTrip] { trips.sorted { ($0.trip?.stops.count ?? 0) > ($1.trip?.stops.count ?? 0) } } /// Trips as domain objects for poll creation private var tripsForPollCreation: [Trip] { trips.compactMap { $0.trip } } var body: some View { ScrollView { LazyVStack(spacing: Theme.Spacing.lg) { // Polls Section pollsSection // Trips Section tripsSection } .padding(Theme.Spacing.md) } .themedBackground() .task { await loadPolls() } .refreshable { await loadPolls() } .sheet(isPresented: $showCreatePoll) { PollCreationView(trips: tripsForPollCreation) { poll in polls.insert(poll, at: 0) } } .navigationDestination(for: TripPoll.self) { poll in PollDetailView(pollId: poll.id) } } // MARK: - Polls Section @ViewBuilder private var pollsSection: some View { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { HStack { Text("Group Polls") .font(.title2) .foregroundStyle(Theme.textPrimary(colorScheme)) Spacer() if trips.count >= 2 { Button { showCreatePoll = true } label: { Image(systemName: "plus.circle") .foregroundStyle(Theme.warmOrange) } } } if isLoadingPolls { ProgressView() .frame(maxWidth: .infinity, alignment: .center) .padding() } else if polls.isEmpty { emptyPollsCard } else { ForEach(polls) { poll in NavigationLink(value: poll) { PollRowCard(poll: poll) } .buttonStyle(.plain) } } } } @ViewBuilder private var emptyPollsCard: some View { VStack(spacing: Theme.Spacing.sm) { Image(systemName: "person.3") .font(.title) .foregroundStyle(Theme.textMuted(colorScheme)) Text("No group polls yet") .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) if trips.count >= 2 { Text("Create a poll to let friends vote on trip options") .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) .multilineTextAlignment(.center) } else { Text("Save at least 2 trips to create a poll") .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) .multilineTextAlignment(.center) } } .frame(maxWidth: .infinity) .padding(Theme.Spacing.lg) .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) .overlay { RoundedRectangle(cornerRadius: Theme.CornerRadius.large) .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) } } // MARK: - Trips Section @ViewBuilder private var tripsSection: some View { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { Text("Saved Trips") .font(.title2) .foregroundStyle(Theme.textPrimary(colorScheme)) if trips.isEmpty { VStack(spacing: 16) { Image(systemName: "suitcase") .font(.largeTitle) .foregroundColor(.secondary) Text("No Saved Trips") .font(.headline) Text("Browse featured trips on the Home tab or create your own to get started.") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) .padding(Theme.Spacing.xl) .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) } else { ForEach(Array(sortedTrips.enumerated()), id: \.element.id) { index, savedTrip in if let trip = savedTrip.trip { NavigationLink { TripDetailView(trip: trip, games: savedTrip.games) } label: { SavedTripListRow(trip: trip) } .buttonStyle(.plain) .staggeredAnimation(index: index) } } } } } // MARK: - Actions private func loadPolls() async { isLoadingPolls = true do { polls = try await PollService.shared.fetchMyPolls() } catch { // Silently fail - polls just won't show } isLoadingPolls = false } } // MARK: - Poll Row Card private struct PollRowCard: View { let poll: TripPoll @Environment(\.colorScheme) private var colorScheme var body: some View { HStack(spacing: Theme.Spacing.md) { // Icon ZStack { Circle() .fill(Theme.warmOrange.opacity(0.15)) .frame(width: 44, height: 44) Image(systemName: "chart.bar.doc.horizontal") .foregroundStyle(Theme.warmOrange) } VStack(alignment: .leading, spacing: Theme.Spacing.xs) { Text(poll.title) .font(.headline) .foregroundStyle(Theme.textPrimary(colorScheme)) HStack(spacing: Theme.Spacing.sm) { Label("\(poll.tripSnapshots.count) trips", systemImage: "map") Text("•") Text(poll.shareCode) .fontWeight(.semibold) .foregroundStyle(Theme.warmOrange) } .font(.caption) .foregroundStyle(Theme.textSecondary(colorScheme)) } Spacer() Image(systemName: "chevron.right") .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) } .padding(Theme.Spacing.md) .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: 6, y: 3) } } struct SavedTripListRow: View { let trip: Trip @Environment(\.colorScheme) private var colorScheme var body: some View { HStack(spacing: Theme.Spacing.md) { // Route preview VStack(spacing: 4) { ForEach(0.. Void @Environment(\.colorScheme) private var colorScheme var body: some View { VStack(spacing: Theme.Spacing.xl) { Spacer() ZStack { Circle() .fill(Theme.warmOrange.opacity(0.15)) .frame(width: 100, height: 100) Image(systemName: "lock.fill") .font(.system(size: 40)) .foregroundStyle(Theme.warmOrange) } VStack(spacing: Theme.Spacing.sm) { Text(feature.displayName) .font(.title2.bold()) .foregroundStyle(Theme.textPrimary(colorScheme)) Text(feature.description) .font(.body) .foregroundStyle(Theme.textSecondary(colorScheme)) .multilineTextAlignment(.center) .padding(.horizontal, Theme.Spacing.xl) } Button { onUnlock() } label: { HStack { Image(systemName: "star.fill") Text("Upgrade to Pro") } .fontWeight(.semibold) .frame(maxWidth: .infinity) .padding(Theme.Spacing.md) .background(Theme.warmOrange) .foregroundStyle(.white) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) } .padding(.horizontal, Theme.Spacing.xl) Spacer() Spacer() } .themedBackground() } } #Preview { HomeView() .modelContainer(for: SavedTrip.self, inMemory: true) }