// // TeamFirstWizardStep.swift // SportsTime // // Wizard step for Team-First planning mode. // Uses sheet-based drill-down matching TeamPickerStep: Sport → Teams (multi-select). // import SwiftUI struct TeamFirstWizardStep: View { @Environment(\.colorScheme) private var colorScheme @Binding var selectedSport: Sport? @Binding var selectedTeamIds: Set @State private var showTeamPicker = false private var selectedTeams: [Team] { selectedTeamIds.compactMap { teamId in AppDataProvider.shared.teams.first { $0.id == teamId } }.sorted { $0.fullName < $1.fullName } } private var isValid: Bool { selectedTeamIds.count >= 2 } var body: some View { VStack(alignment: .leading, spacing: Theme.Spacing.md) { StepHeader( title: "Which teams do you want to see?", subtitle: "Select 2 or more teams to find optimal trip windows" ) if !selectedTeams.isEmpty { HStack(spacing: Theme.Spacing.sm) { Button { showTeamPicker = true } label: { HStack { teamPreview Spacer() } .contentShape(Rectangle()) } .buttonStyle(.plain) Button { selectedTeamIds.removeAll() selectedSport = nil } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(Theme.textMuted(colorScheme)) } .buttonStyle(.plain) .minimumHitTarget() .accessibilityLabel("Clear all teams") } .padding(Theme.Spacing.md) .background(Theme.cardBackgroundElevated(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) .overlay( RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) .stroke(Theme.warmOrange, lineWidth: 2) ) } else { // Selection button Button { showTeamPicker = true } label: { HStack { Image(systemName: "person.2.fill") .foregroundStyle(Theme.warmOrange) .accessibilityHidden(true) Text("Select teams") .font(.subheadline) .foregroundStyle(Theme.textMuted(colorScheme)) Spacer() Image(systemName: "chevron.right") .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) .accessibilityHidden(true) } .padding(Theme.Spacing.md) .background(Theme.cardBackgroundElevated(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) .overlay( RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) .stroke(Theme.textMuted(colorScheme).opacity(0.3), lineWidth: 1) ) } .buttonStyle(.plain) } // Validation message if selectedTeamIds.isEmpty { validationLabel(text: "Select at least 2 teams", isValid: false) } else if selectedTeamIds.count == 1 { validationLabel(text: "Select 1 more team (minimum 2)", isValid: false) } else { validationLabel(text: "Ready to find trips for \(selectedTeamIds.count) teams", isValid: true) } } .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) } .sheet(isPresented: $showTeamPicker) { TeamFirstPickerSheet( selectedSport: $selectedSport, selectedTeamIds: $selectedTeamIds, isPresented: $showTeamPicker ) } } // MARK: - Team Preview @ViewBuilder private var teamPreview: some View { if selectedTeams.count <= 3 { // Show individual teams HStack(spacing: Theme.Spacing.sm) { ForEach(selectedTeams.prefix(3)) { team in HStack(spacing: Theme.Spacing.xs) { Circle() .fill(team.primaryColor.map { Color(hex: $0) } ?? team.sport.themeColor) .frame(width: 20, height: 20) Text(team.abbreviation) .font(.caption) .fontWeight(.medium) .foregroundStyle(Theme.textPrimary(colorScheme)) } .padding(.horizontal, Theme.Spacing.sm) .padding(.vertical, Theme.Spacing.xs) .background(Theme.cardBackground(colorScheme)) .clipShape(Capsule()) } } } else { // Show count with first few team colors HStack(spacing: -8) { ForEach(Array(selectedTeams.prefix(4).enumerated()), id: \.element.id) { index, team in Circle() .fill(team.primaryColor.map { Color(hex: $0) } ?? team.sport.themeColor) .frame(width: 24, height: 24) .overlay( Circle() .stroke(Theme.cardBackgroundElevated(colorScheme), lineWidth: 2) ) .zIndex(Double(4 - index)) } } .accessibilityHidden(true) Text("\(selectedTeamIds.count) teams") .font(.subheadline) .fontWeight(.medium) .foregroundStyle(Theme.textPrimary(colorScheme)) } } private func validationLabel(text: String, isValid: Bool) -> some View { HStack(spacing: Theme.Spacing.xs) { Image(systemName: isValid ? "checkmark.circle.fill" : "info.circle.fill") .foregroundStyle(isValid ? .green : Theme.warmOrange) Text(text) .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) Spacer() } } } // MARK: - Team First Picker Sheet private struct TeamFirstPickerSheet: View { @Environment(\.colorScheme) private var colorScheme @Binding var selectedSport: Sport? @Binding var selectedTeamIds: Set @Binding var isPresented: Bool var body: some View { NavigationStack { List { ForEach(Sport.supported, id: \.self) { sport in NavigationLink { TeamMultiSelectListView( sport: sport, selectedTeamIds: $selectedTeamIds, onDone: { selectedSport = sport isPresented = false } ) } label: { HStack(spacing: Theme.Spacing.sm) { Image(systemName: sport.iconName) .font(.title2) .foregroundStyle(sport.themeColor) .frame(width: 32) Text(sport.rawValue) .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) Spacer() Text("\(teamsCount(for: sport)) teams") .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) } .padding(.vertical, Theme.Spacing.xs) } } } .listStyle(.plain) .navigationTitle("Select Sport") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { isPresented = false } } } } .presentationDetents([.large]) } private func teamsCount(for sport: Sport) -> Int { AppDataProvider.shared.teams.filter { $0.sport == sport }.count } } // MARK: - Team Multi-Select List View private struct TeamMultiSelectListView: View { @Environment(\.colorScheme) private var colorScheme let sport: Sport @Binding var selectedTeamIds: Set let onDone: () -> Void @State private var searchText = "" private var teams: [Team] { let allTeams = AppDataProvider.shared.teams .filter { $0.sport == sport } .sorted { $0.fullName < $1.fullName } if searchText.isEmpty { return allTeams } return allTeams.filter { $0.fullName.localizedCaseInsensitiveContains(searchText) || $0.city.localizedCaseInsensitiveContains(searchText) } } private var selectionCount: Int { // Count only teams from the current sport let sportTeamIds = Set(AppDataProvider.shared.teams.filter { $0.sport == sport }.map { $0.id }) return selectedTeamIds.intersection(sportTeamIds).count } var body: some View { List { ForEach(teams) { team in Button { toggleTeam(team) } label: { HStack(spacing: Theme.Spacing.sm) { Circle() .fill(team.primaryColor.map { Color(hex: $0) } ?? sport.themeColor) .frame(width: 28, height: 28) VStack(alignment: .leading, spacing: 2) { Text(team.fullName) .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) Text(team.city) .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) } Spacer() if selectedTeamIds.contains(team.id) { Image(systemName: "checkmark.circle.fill") .foregroundStyle(Theme.warmOrange) .accessibilityHidden(true) } else { Image(systemName: "circle") .foregroundStyle(Theme.textMuted(colorScheme).opacity(0.5)) .accessibilityHidden(true) } } .padding(.vertical, Theme.Spacing.xs) } .buttonStyle(.plain) .accessibilityAddTraits(selectedTeamIds.contains(team.id) ? .isSelected : []) } } .listStyle(.plain) .searchable(text: $searchText, prompt: "Search teams") .navigationTitle(sport.rawValue) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { Button { onDone() } label: { if selectionCount >= 2 { Text("Done (\(selectionCount))") .fontWeight(.semibold) } else { Text("Done") } } .disabled(selectionCount < 2) } } .onAppear { // Clear selections from other sports when entering let sportTeamIds = Set(AppDataProvider.shared.teams.filter { $0.sport == sport }.map { $0.id }) selectedTeamIds = selectedTeamIds.intersection(sportTeamIds) } } private func toggleTeam(_ team: Team) { Theme.Animation.withMotion(.easeInOut(duration: 0.15)) { if selectedTeamIds.contains(team.id) { selectedTeamIds.remove(team.id) } else { selectedTeamIds.insert(team.id) } } } } // MARK: - Preview #Preview("Empty") { TeamFirstWizardStep( selectedSport: .constant(nil), selectedTeamIds: .constant([]) ) .padding() .themedBackground() } #Preview("Teams Selected") { TeamFirstWizardStep( selectedSport: .constant(.mlb), selectedTeamIds: .constant(["team_mlb_bos", "team_mlb_nyy", "team_mlb_chc"]) ) .padding() .themedBackground() }