Add team autocomplete and themed backgrounds to Log Visit sheet

- Add autocomplete suggestions for Home/Away team fields filtered by selected sport
- Apply themed background to StadiumVisitSheet and StadiumPickerSheet
- Add listRowBackground for consistent card styling in Form sections
- Fix data observation with @ObservedObject for AppDataProvider
- Clear team names when sport selection changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-08 20:32:44 -06:00
parent 92d808caf5
commit 588938d2a1
2 changed files with 133 additions and 45 deletions

View File

@@ -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
}
}
}
}

View File

@@ -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)