diff --git a/SportsTime/Core/Theme/AnimatedComponents.swift b/SportsTime/Core/Theme/AnimatedComponents.swift index 8163c59..5482351 100644 --- a/SportsTime/Core/Theme/AnimatedComponents.swift +++ b/SportsTime/Core/Theme/AnimatedComponents.swift @@ -106,52 +106,36 @@ struct AnimatedRouteGraphic: View { // MARK: - Themed Spinner /// A custom animated spinner matching the app's visual style +/// Both ThemedSpinner and ThemedSpinnerCompact use the same visual style for consistency struct ThemedSpinner: View { var size: CGFloat = 40 var lineWidth: CGFloat = 4 + var color: Color = Theme.warmOrange @State private var rotation: Double = 0 - @State private var trimEnd: CGFloat = 0.6 var body: some View { ZStack { // Background track Circle() - .stroke(Theme.warmOrange.opacity(0.15), lineWidth: lineWidth) + .stroke(color.opacity(0.2), lineWidth: lineWidth) // Animated arc Circle() - .trim(from: 0, to: trimEnd) - .stroke( - AngularGradient( - gradient: Gradient(colors: [Theme.warmOrange, Theme.routeGold, Theme.warmOrange.opacity(0.3)]), - center: .center, - startAngle: .degrees(0), - endAngle: .degrees(360) - ), - style: StrokeStyle(lineWidth: lineWidth, lineCap: .round) - ) + .trim(from: 0, to: 0.7) + .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) .rotationEffect(.degrees(rotation)) - - // Center glow dot - Circle() - .fill(Theme.warmOrange.opacity(0.2)) - .frame(width: size * 0.3, height: size * 0.3) - .blur(radius: 4) } .frame(width: size, height: size) .onAppear { - withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) { + withAnimation(.linear(duration: 0.8).repeatForever(autoreverses: false)) { rotation = 360 } - withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) { - trimEnd = 0.8 - } } } } -/// Compact themed spinner for inline use +/// Compact themed spinner for inline use (same style as ThemedSpinner, smaller default) struct ThemedSpinnerCompact: View { var size: CGFloat = 20 var color: Color = Theme.warmOrange @@ -159,16 +143,23 @@ struct ThemedSpinnerCompact: View { @State private var rotation: Double = 0 var body: some View { - Circle() - .trim(from: 0, to: 0.7) - .stroke(color, style: StrokeStyle(lineWidth: size > 16 ? 2.5 : 2, lineCap: .round)) - .frame(width: size, height: size) - .rotationEffect(.degrees(rotation)) - .onAppear { - withAnimation(.linear(duration: 0.8).repeatForever(autoreverses: false)) { - rotation = 360 - } + ZStack { + // Background track + Circle() + .stroke(color.opacity(0.2), lineWidth: size > 16 ? 2.5 : 2) + + // Animated arc + Circle() + .trim(from: 0, to: 0.7) + .stroke(color, style: StrokeStyle(lineWidth: size > 16 ? 2.5 : 2, lineCap: .round)) + .rotationEffect(.degrees(rotation)) + } + .frame(width: size, height: size) + .onAppear { + withAnimation(.linear(duration: 0.8).repeatForever(autoreverses: false)) { + rotation = 360 } + } } } diff --git a/SportsTime/Features/Progress/Views/StadiumVisitSheet.swift b/SportsTime/Features/Progress/Views/StadiumVisitSheet.swift index 41685b2..74e5589 100644 --- a/SportsTime/Features/Progress/Views/StadiumVisitSheet.swift +++ b/SportsTime/Features/Progress/Views/StadiumVisitSheet.swift @@ -34,9 +34,35 @@ struct StadiumVisitSheet: View { @State private var showStadiumPicker = false @State private var isSaving = false @State private var errorMessage: String? + @State private var showAwayTeamSuggestions = false + @State private var showHomeTeamSuggestions = false + @FocusState private var awayTeamFocused: Bool + @FocusState private var homeTeamFocused: Bool // Data - private let dataProvider = AppDataProvider.shared + @ObservedObject private var dataProvider = AppDataProvider.shared + + // Teams for autocomplete + private var teamsForSport: [Team] { + dataProvider.teams.filter { $0.sport == selectedSport } + .sorted { $0.name < $1.name } + } + + private var awayTeamSuggestions: [Team] { + guard !awayTeamName.isEmpty else { return teamsForSport } + return teamsForSport.filter { + $0.name.localizedCaseInsensitiveContains(awayTeamName) || + $0.city.localizedCaseInsensitiveContains(awayTeamName) + } + } + + private var homeTeamSuggestions: [Team] { + guard !homeTeamName.isEmpty else { return teamsForSport } + return teamsForSport.filter { + $0.name.localizedCaseInsensitiveContains(homeTeamName) || + $0.city.localizedCaseInsensitiveContains(homeTeamName) + } + } init( initialStadium: Stadium? = nil, @@ -89,6 +115,7 @@ struct StadiumVisitSheet: View { } header: { Text("Location") } + .listRowBackground(Theme.cardBackground(colorScheme)) // Visit Details Section Section { @@ -102,22 +129,81 @@ struct StadiumVisitSheet: View { } header: { Text("Visit Details") } + .listRowBackground(Theme.cardBackground(colorScheme)) // Game Info Section (only for game visits) if visitType == .game { Section { - HStack { - Text("Away Team") - Spacer() - TextField("Team Name", text: $awayTeamName) - .multilineTextAlignment(.trailing) + // Away Team with autocomplete + VStack(alignment: .leading, spacing: 0) { + HStack { + Text("Away Team") + Spacer() + TextField("Team Name", text: $awayTeamName) + .multilineTextAlignment(.trailing) + .focused($awayTeamFocused) + .onChange(of: awayTeamFocused) { _, focused in + showAwayTeamSuggestions = focused + } + } + + if showAwayTeamSuggestions && !awayTeamSuggestions.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(awayTeamSuggestions.prefix(10)) { team in + Button { + awayTeamName = team.name + awayTeamFocused = false + } label: { + Text(team.name) + .font(.system(size: Theme.FontSize.caption)) + .foregroundStyle(Theme.textPrimary(colorScheme)) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(Theme.cardBackgroundElevated(colorScheme)) + .clipShape(Capsule()) + } + } + } + .padding(.top, 8) + } + } } - HStack { - Text("Home Team") - Spacer() - TextField("Team Name", text: $homeTeamName) - .multilineTextAlignment(.trailing) + // Home Team with autocomplete + VStack(alignment: .leading, spacing: 0) { + HStack { + Text("Home Team") + Spacer() + TextField("Team Name", text: $homeTeamName) + .multilineTextAlignment(.trailing) + .focused($homeTeamFocused) + .onChange(of: homeTeamFocused) { _, focused in + showHomeTeamSuggestions = focused + } + } + + if showHomeTeamSuggestions && !homeTeamSuggestions.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(homeTeamSuggestions.prefix(10)) { team in + Button { + homeTeamName = team.name + homeTeamFocused = false + } label: { + Text(team.name) + .font(.system(size: Theme.FontSize.caption)) + .foregroundStyle(Theme.textPrimary(colorScheme)) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(Theme.cardBackgroundElevated(colorScheme)) + .clipShape(Capsule()) + } + } + } + .padding(.top, 8) + } + } } HStack { @@ -139,6 +225,7 @@ struct StadiumVisitSheet: View { } footer: { Text("Leave blank if you don't remember the score") } + .listRowBackground(Theme.cardBackground(colorScheme)) } // Optional Details Section @@ -161,6 +248,7 @@ struct StadiumVisitSheet: View { } header: { Text("Additional Info") } + .listRowBackground(Theme.cardBackground(colorScheme)) // Error Message if let error = errorMessage { @@ -172,9 +260,12 @@ struct StadiumVisitSheet: View { .foregroundStyle(.red) } } + .listRowBackground(Theme.cardBackground(colorScheme)) } } .scrollDismissesKeyboard(.interactively) + .scrollContentBackground(.hidden) + .themedBackground() .navigationTitle("Log Visit") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -199,7 +290,7 @@ struct StadiumVisitSheet: View { ) } .onChange(of: selectedSport) { _, _ in - // Clear stadium selection when sport changes + // Clear selections when sport changes if let stadium = selectedStadium { // Check if stadium belongs to new sport let sportTeams = dataProvider.teams.filter { $0.sport == selectedSport } @@ -207,6 +298,9 @@ struct StadiumVisitSheet: View { selectedStadium = nil } } + // Clear team names when sport changes + homeTeamName = "" + awayTeamName = "" } } } @@ -277,7 +371,7 @@ struct StadiumPickerSheet: View { @Environment(\.dismiss) private var dismiss @State private var searchText = "" - private let dataProvider = AppDataProvider.shared + @ObservedObject private var dataProvider = AppDataProvider.shared private var stadiums: [Stadium] { let sportTeams = dataProvider.teams.filter { $0.sport == sport } @@ -331,10 +425,13 @@ struct StadiumPickerSheet: View { } } } + .listRowBackground(Theme.cardBackground(colorScheme)) } .scrollDismissesKeyboard(.interactively) + .scrollContentBackground(.hidden) } } + .themedBackground() .searchable(text: $searchText, prompt: "Search stadiums") .navigationTitle("Select Stadium") .navigationBarTitleDisplayMode(.inline)