// // GameMatchConfirmationView.swift // SportsTime // // View for confirming/selecting the correct game match from photo import. // import SwiftUI // MARK: - Game Match Confirmation View struct GameMatchConfirmationView: View { let candidate: PhotoImportCandidate let onConfirm: (GameMatchCandidate) -> Void let onSkip: () -> Void @Environment(\.colorScheme) private var colorScheme @Environment(\.dismiss) private var dismiss @State private var selectedMatch: GameMatchCandidate? var body: some View { NavigationStack { ScrollView { VStack(spacing: Theme.Spacing.lg) { // Photo info header photoInfoHeader .staggeredAnimation(index: 0) // Stadium info if let stadium = candidate.bestStadiumMatch { stadiumCard(stadium) .staggeredAnimation(index: 1) } // Match options matchOptionsSection .staggeredAnimation(index: 2) // Action buttons actionButtons .staggeredAnimation(index: 3) } .padding(Theme.Spacing.md) } .themedBackground() .navigationTitle("Confirm Game") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } } .onAppear { // Pre-select if single match if case .singleMatch(let match) = candidate.matchResult { selectedMatch = match } } } // MARK: - Photo Info Header private var photoInfoHeader: some View { VStack(spacing: Theme.Spacing.md) { // Icon ZStack { Circle() .fill(Theme.warmOrange.opacity(0.15)) .frame(width: 80, height: 80) Image(systemName: "photo.fill") .font(.largeTitle) .foregroundStyle(Theme.warmOrange) } VStack(spacing: Theme.Spacing.xs) { if let date = candidate.metadata.captureDate { Label(formatDate(date), systemImage: "calendar") .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) } if candidate.metadata.hasValidLocation { Label("Location data available", systemImage: "location.fill") .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) } else { Label("No location data", systemImage: "location.slash") .font(.subheadline) .foregroundStyle(.red) } } } .frame(maxWidth: .infinity) .padding(Theme.Spacing.lg) .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) } // MARK: - Stadium Card private func stadiumCard(_ match: StadiumMatch) -> some View { VStack(alignment: .leading, spacing: Theme.Spacing.md) { HStack { Image(systemName: "mappin.circle.fill") .foregroundStyle(Theme.warmOrange) .accessibilityHidden(true) Text("Nearest Stadium") .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) Spacer() } HStack { VStack(alignment: .leading, spacing: 4) { Text(match.stadium.name) .font(.headline) .foregroundStyle(Theme.textPrimary(colorScheme)) Text(match.stadium.fullAddress) .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) } Spacer() // Distance badge VStack(spacing: 2) { Text(match.formattedDistance) .font(.subheadline) .foregroundStyle(confidenceColor(match.confidence)) .accessibilityLabel("\(match.formattedDistance), \(match.confidence.description) confidence") Text(match.confidence.description) .font(.caption2) .foregroundStyle(Theme.textMuted(colorScheme)) } } } .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: - Match Options Section private var matchOptionsSection: some View { VStack(alignment: .leading, spacing: Theme.Spacing.md) { HStack { Image(systemName: "sportscourt.fill") .foregroundStyle(Theme.warmOrange) .accessibilityHidden(true) Text(matchOptionsTitle) .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) Spacer() } matchOptionsContent } .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) } } private var matchOptionsTitle: String { switch candidate.matchResult { case .singleMatch: return "Matched Game" case .multipleMatches(let matches): return "Select Game (\(matches.count) options)" case .noMatches: return "No Games Found" } } @ViewBuilder private var matchOptionsContent: some View { switch candidate.matchResult { case .singleMatch(let match): gameMatchRow(match, isSelected: true) case .multipleMatches(let matches): VStack(spacing: Theme.Spacing.sm) { ForEach(matches) { match in Button { selectedMatch = match } label: { gameMatchRow(match, isSelected: selectedMatch?.id == match.id) } .buttonStyle(.plain) .accessibilityValue(selectedMatch?.id == match.id ? "Selected" : "Not selected") .accessibilityAddTraits(selectedMatch?.id == match.id ? .isSelected : []) } } case .noMatches(let reason): HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.red) Text(reason.description) .font(.body) .foregroundStyle(Theme.textSecondary(colorScheme)) } .padding(Theme.Spacing.md) } } private func gameMatchRow(_ match: GameMatchCandidate, isSelected: Bool) -> some View { HStack { VStack(alignment: .leading, spacing: 4) { HStack { Text(match.fullMatchupDescription) .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) Image(systemName: match.game.sport.iconName) .font(.caption) .foregroundStyle(match.game.sport.themeColor) .accessibilityHidden(true) } Text(match.gameDateTime) .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) // Confidence HStack(spacing: 4) { Circle() .fill(combinedConfidenceColor(match.confidence.combined)) .frame(width: 8, height: 8) .accessibilityLabel(confidenceAccessibilityLabel(match.confidence.combined)) Text(match.confidence.combined.description) .font(.caption2) .foregroundStyle(Theme.textMuted(colorScheme)) } } Spacer() // Selection indicator Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") .font(.title2) .foregroundStyle(isSelected ? .green : Theme.textMuted(colorScheme)) .accessibilityHidden(true) } .padding(Theme.Spacing.md) .background(isSelected ? Theme.cardBackgroundElevated(colorScheme) : Color.clear) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.small)) .overlay { if isSelected { RoundedRectangle(cornerRadius: Theme.CornerRadius.small) .stroke(.green.opacity(0.5), lineWidth: 2) } } } // MARK: - Action Buttons private var actionButtons: some View { VStack(spacing: Theme.Spacing.sm) { // Confirm button Button { if let match = selectedMatch { onConfirm(match) dismiss() } } label: { HStack { Image(systemName: "checkmark.circle.fill") Text("Confirm & Import") } .font(.body) .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding(Theme.Spacing.md) .background(selectedMatch != nil ? .green : Theme.textMuted(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) } .disabled(selectedMatch == nil) // Skip button Button { onSkip() dismiss() } label: { Text("Skip This Photo") .font(.body) .foregroundStyle(Theme.textSecondary(colorScheme)) } } } // MARK: - Helpers private func formatDate(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter.string(from: date) } private func confidenceColor(_ confidence: MatchConfidence) -> Color { switch confidence { case .high: return .green case .medium: return Theme.warmOrange case .low: return .red case .none: return Theme.textMuted(colorScheme) } } private func combinedConfidenceColor(_ confidence: CombinedConfidence) -> Color { switch confidence { case .autoSelect: return .green case .userConfirm: return Theme.warmOrange case .manualOnly: return .red } } private func confidenceAccessibilityLabel(_ confidence: CombinedConfidence) -> String { switch confidence { case .autoSelect: return "High confidence" case .userConfirm: return "Medium confidence" case .manualOnly: return "Low confidence" } } } // MARK: - Preview #Preview { let metadata = PhotoMetadata( captureDate: Date(), coordinates: nil ) let candidate = PhotoImportCandidate( metadata: metadata, matchResult: .noMatches(.metadataMissing(.noLocation)), stadiumMatches: [] ) GameMatchConfirmationView( candidate: candidate, onConfirm: { _ in }, onSkip: {} ) }