// // ProgressTabView.swift // SportsTime // // Main view for stadium progress tracking with league selector and map. // import SwiftUI import SwiftData struct ProgressTabView: View { @Environment(\.modelContext) private var modelContext @Environment(\.colorScheme) private var colorScheme @State private var viewModel = ProgressViewModel() @State private var showVisitSheet = false @State private var showPhotoImport = false @State private var showShareSheet = false @State private var selectedStadium: Stadium? @State private var selectedVisitId: UUID? @Query private var visits: [StadiumVisit] var body: some View { ScrollView { VStack(spacing: Theme.Spacing.lg) { // League Selector leagueSelector .staggeredAnimation(index: 0) // Progress Summary Card progressSummaryCard .staggeredAnimation(index: 1) // Map View ProgressMapView( stadiums: viewModel.sportStadiums, visitStatus: viewModel.stadiumVisitStatus, selectedStadium: $selectedStadium ) .frame(height: 300) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) .overlay { RoundedRectangle(cornerRadius: Theme.CornerRadius.large) .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) } .staggeredAnimation(index: 2) // Stadium Lists stadiumListsSection .staggeredAnimation(index: 3) // Achievements Teaser achievementsSection .staggeredAnimation(index: 4) // Recent Visits if !viewModel.recentVisits.isEmpty { recentVisitsSection .staggeredAnimation(index: 5) } } .padding(Theme.Spacing.md) } .themedBackground() .toolbar { ToolbarItem(placement: .topBarLeading) { Button { showShareSheet = true } label: { Image(systemName: "square.and.arrow.up") .foregroundStyle(Theme.warmOrange) } } ToolbarItem(placement: .primaryAction) { Menu { Button { showVisitSheet = true } label: { Label("Manual Entry", systemImage: "pencil") } Button { showPhotoImport = true } label: { Label("Import from Photos", systemImage: "photo.on.rectangle.angled") } } label: { Image(systemName: "plus.circle.fill") .font(.title2) .foregroundStyle(Theme.warmOrange) } } } .task { viewModel.configure(with: modelContext.container) await viewModel.loadData() } .sheet(isPresented: $showVisitSheet) { StadiumVisitSheet(initialSport: viewModel.selectedSport) { _ in Task { await viewModel.loadData() } } } .sheet(isPresented: $showPhotoImport) { PhotoImportView() .onDisappear { Task { await viewModel.loadData() } } } .sheet(item: $selectedStadium) { stadium in StadiumDetailSheet( stadium: stadium, visitStatus: viewModel.stadiumVisitStatus[stadium.id] ?? .notVisited, sport: viewModel.selectedSport, onVisitLogged: { Task { await viewModel.loadData() } } ) .presentationDetents([.medium]) } .sheet(isPresented: $showShareSheet) { ProgressShareView(progress: viewModel.leagueProgress) } } // MARK: - League Selector private var leagueSelector: some View { SportSelectorGrid { sport in SportProgressButton( sport: sport, isSelected: viewModel.selectedSport == sport, progress: progressForSport(sport) ) { withAnimation(Theme.Animation.spring) { viewModel.selectSport(sport) } } } } private func progressForSport(_ sport: Sport) -> Double { let visitedCount = viewModel.visits.filter { $0.sportEnum == sport }.count let total = LeagueStructure.stadiumCount(for: sport) guard total > 0 else { return 0 } return Double(min(visitedCount, total)) / Double(total) } // MARK: - Progress Summary Card private var progressSummaryCard: some View { let progress = viewModel.leagueProgress return VStack(spacing: Theme.Spacing.lg) { // Title and progress ring HStack(alignment: .center, spacing: Theme.Spacing.lg) { // Progress Ring ZStack { Circle() .stroke(Theme.warmOrange.opacity(0.2), lineWidth: 8) .frame(width: 80, height: 80) Circle() .trim(from: 0, to: progress.progressFraction) .stroke(Theme.warmOrange, style: StrokeStyle(lineWidth: 8, lineCap: .round)) .frame(width: 80, height: 80) .rotationEffect(.degrees(-90)) .animation(.easeInOut(duration: 0.5), value: progress.progressFraction) VStack(spacing: 0) { Text("\(progress.visitedStadiums)") .font(.title3) .foregroundStyle(Theme.textPrimary(colorScheme)) Text("/\(progress.totalStadiums)") .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) } } VStack(alignment: .leading, spacing: Theme.Spacing.xs) { Text(viewModel.selectedSport.displayName) .font(.headline) .foregroundStyle(Theme.textPrimary(colorScheme)) Text("Stadium Quest") .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) if progress.isComplete { HStack(spacing: 4) { Image(systemName: "checkmark.seal.fill") Text("Complete!") } .font(.subheadline) .foregroundStyle(Theme.warmOrange) } else { Text("\(progress.totalStadiums - progress.visitedStadiums) stadiums remaining") .font(.subheadline) .foregroundStyle(Theme.textMuted(colorScheme)) } } Spacer() } // Stats row HStack(spacing: Theme.Spacing.lg) { ProgressStatPill( icon: "mappin.circle.fill", value: "\(progress.visitedStadiums)", label: "Visited" ) ProgressStatPill( icon: "circle.dotted", value: "\(progress.totalStadiums - progress.visitedStadiums)", label: "Remaining" ) ProgressStatPill( icon: "percent", value: String(format: "%.0f%%", progress.completionPercentage), label: "Complete" ) } } .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) } .shadow(color: Theme.cardShadow(colorScheme), radius: 10, y: 5) } // MARK: - Stadium Lists Section private var stadiumListsSection: some View { VStack(alignment: .leading, spacing: Theme.Spacing.md) { // Visited Stadiums if !viewModel.visitedStadiums.isEmpty { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { HStack { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) Text("Visited (\(viewModel.visitedStadiums.count))") .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) } ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: Theme.Spacing.sm) { ForEach(viewModel.visitedStadiums) { stadium in StadiumChip( stadium: stadium, isVisited: true, visitCount: viewModel.stadiumVisitStatus[stadium.id]?.visitCount ?? 1 ) { selectedStadium = stadium } } } } } } // Unvisited Stadiums if !viewModel.unvisitedStadiums.isEmpty { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { HStack { Image(systemName: "circle.dotted") .foregroundStyle(Theme.textMuted(colorScheme)) Text("Not Yet Visited (\(viewModel.unvisitedStadiums.count))") .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) } ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: Theme.Spacing.sm) { ForEach(viewModel.unvisitedStadiums) { stadium in StadiumChip( stadium: stadium, isVisited: false ) { selectedStadium = stadium } } } } } } } } // MARK: - Achievements Section private var achievementsSection: some View { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { HStack { Text("Achievements") .font(.title2) .foregroundStyle(Theme.textPrimary(colorScheme)) Spacer() NavigationLink { AchievementsListView() } label: { HStack(spacing: 4) { Text("View All") Image(systemName: "chevron.right") } .font(.subheadline) .foregroundStyle(Theme.warmOrange) } } NavigationLink { AchievementsListView() } label: { HStack(spacing: Theme.Spacing.md) { // Trophy icon ZStack { Circle() .fill(Theme.warmOrange.opacity(0.15)) .frame(width: 50, height: 50) Image(systemName: "trophy.fill") .font(.system(size: 24)) .foregroundStyle(Theme.warmOrange) } VStack(alignment: .leading, spacing: 4) { Text("Track Your Progress") .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) Text("Earn badges for stadium visits") .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) } Spacer() Image(systemName: "chevron.right") .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.warmOrange.opacity(0.3), lineWidth: 1) } } .buttonStyle(.plain) } } // MARK: - Recent Visits Section private var recentVisitsSection: some View { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { HStack { Text("Recent Visits") .font(.title2) .foregroundStyle(Theme.textPrimary(colorScheme)) Spacer() NavigationLink { GamesHistoryView() } label: { HStack(spacing: 4) { Text("See All") Image(systemName: "chevron.right") } .font(.subheadline) .foregroundStyle(Theme.warmOrange) } } ForEach(viewModel.recentVisits) { visitSummary in if let stadiumVisit = visits.first(where: { $0.id == visitSummary.id }) { NavigationLink { VisitDetailView(visit: stadiumVisit, stadium: visitSummary.stadium) } label: { RecentVisitRow(visit: visitSummary) } .buttonStyle(.plain) } else { RecentVisitRow(visit: visitSummary) } } } } } // MARK: - Supporting Views struct ProgressStatPill: View { let icon: String let value: String let label: String @Environment(\.colorScheme) private var colorScheme var body: some View { VStack(spacing: 4) { HStack(spacing: 4) { Image(systemName: icon) .font(.caption) Text(value) .font(.body) } .foregroundStyle(Theme.textPrimary(colorScheme)) Text(label) .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) } .frame(maxWidth: .infinity) } } struct StadiumChip: View { let stadium: Stadium let isVisited: Bool var visitCount: Int = 1 let action: () -> Void @Environment(\.colorScheme) private var colorScheme var body: some View { Button(action: action) { HStack(spacing: Theme.Spacing.xs) { if isVisited { Image(systemName: "checkmark.circle.fill") .font(.caption) .foregroundStyle(.green) } VStack(alignment: .leading, spacing: 2) { HStack(spacing: 4) { Text(stadium.name) .font(.subheadline) .foregroundStyle(Theme.textPrimary(colorScheme)) .lineLimit(1) // Visit count badge (if more than 1) if visitCount > 1 { Text("\(visitCount)") .font(.caption2.bold()) .foregroundStyle(.white) .padding(.horizontal, 6) .padding(.vertical, 2) .background(Theme.warmOrange, in: Capsule()) } } Text(stadium.city) .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) } } .padding(.horizontal, Theme.Spacing.sm) .padding(.vertical, Theme.Spacing.xs) .background(Theme.cardBackground(colorScheme)) .clipShape(Capsule()) .overlay { Capsule() .stroke(isVisited ? Color.green.opacity(0.3) : Theme.surfaceGlow(colorScheme), lineWidth: 1) } } .buttonStyle(.plain) } } struct RecentVisitRow: View { let visit: VisitSummary @Environment(\.colorScheme) private var colorScheme var body: some View { HStack(spacing: Theme.Spacing.md) { // Sport icon ZStack { Circle() .fill(visit.sport.themeColor.opacity(0.15)) .frame(width: 40, height: 40) Image(systemName: visit.sport.iconName) .foregroundStyle(visit.sport.themeColor) } VStack(alignment: .leading, spacing: 4) { Text(visit.stadium.name) .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) // Date, Away @ Home on one line, left aligned VStack(alignment: .leading, spacing: 4) { Text(visit.shortDateDescription) if let away = visit.awayTeamName, let home = visit.homeTeamName { Text(away) Text("@") Text(home) } } .font(.subheadline) .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.medium)) .overlay { RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) } } } struct StadiumDetailSheet: View { let stadium: Stadium let visitStatus: StadiumVisitStatus let sport: Sport var onVisitLogged: (() -> Void)? @Environment(\.modelContext) private var modelContext @Environment(\.colorScheme) private var colorScheme @Environment(\.dismiss) private var dismiss @State private var showLogVisit = false var body: some View { NavigationStack { VStack(spacing: Theme.Spacing.lg) { // Stadium header VStack(spacing: Theme.Spacing.sm) { ZStack { Circle() .fill(sport.themeColor.opacity(0.15)) .frame(width: 80, height: 80) Image(systemName: visitStatus.isVisited ? "checkmark.seal.fill" : sport.iconName) .font(.largeTitle) .foregroundStyle(visitStatus.isVisited ? .green : sport.themeColor) } Text(stadium.name) .font(.headline) .foregroundStyle(Theme.textPrimary(colorScheme)) .multilineTextAlignment(.center) Text(stadium.fullAddress) .font(.body) .foregroundStyle(Theme.textSecondary(colorScheme)) if visitStatus.isVisited { HStack(spacing: 4) { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) Text("Visited \(visitStatus.visitCount) time\(visitStatus.visitCount == 1 ? "" : "s")") } .font(.subheadline) .foregroundStyle(.green) } } // Visit history if visited if case .visited(let visits) = visitStatus { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { Text("Visit History") .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) ForEach(visits.sorted(by: { $0.visitDate > $1.visitDate })) { visit in HStack { VStack(alignment: .leading, spacing: 2) { Text(visit.shortDateDescription) .font(.subheadline) if let matchup = visit.matchup { Text(matchup) .font(.caption) .foregroundStyle(Theme.textSecondary(colorScheme)) } } Spacer() Text(visit.visitType.displayName) .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) } .padding(Theme.Spacing.sm) .background(Theme.cardBackgroundElevated(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.small)) } } } Spacer() // Action button Button { showLogVisit = true } label: { HStack { Image(systemName: visitStatus.isVisited ? "plus" : "checkmark.circle") Text(visitStatus.isVisited ? "Log Another Visit" : "Log Visit") } .frame(maxWidth: .infinity) .padding(Theme.Spacing.md) .background(Theme.warmOrange) .foregroundStyle(.white) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) } .pressableStyle() } .padding(Theme.Spacing.lg) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Done") { dismiss() } } } .sheet(isPresented: $showLogVisit) { StadiumVisitSheet( initialStadium: stadium, initialSport: sport ) { _ in onVisitLogged?() dismiss() } } } } } #Preview { NavigationStack { ProgressTabView() } .modelContainer(for: StadiumVisit.self, inMemory: true) }