Files
Sportstime/SportsTime/Features/Trip/Views/Wizard/Steps/LocationSearchSheet.swift
Trey t 1c97f35754 feat: enforce custom Theme colors app-wide, add debug sample trips and poll
Replace all system colors (.secondary, Color(.secondarySystemBackground),
etc.) with Theme.textPrimary/textSecondary/textMuted/cardBackground/
surfaceGlow across 13 views. Remove PostHog debug logging. Add debug
settings for sample trips and hardcoded group poll preview.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 08:54:19 -06:00

160 lines
5.5 KiB
Swift

//
// LocationSearchSheet.swift
// SportsTime
//
// Extracted from TripCreationView - location search sheet for adding cities/places.
//
import SwiftUI
// MARK: - City Input Type
enum CityInputType {
case mustStop
case preferred
case homeLocation
case startLocation
case endLocation
}
// MARK: - Location Search Sheet
struct LocationSearchSheet: View {
let inputType: CityInputType
let onAdd: (LocationInput) -> Void
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
@State private var searchText = ""
@State private var searchResults: [LocationSearchResult] = []
@State private var isSearching = false
@State private var searchTask: Task<Void, Never>?
private let locationService = LocationService.shared
private var navigationTitle: String {
switch inputType {
case .mustStop: return "Add Must-Stop"
case .preferred: return "Add Preferred Location"
case .homeLocation: return "Set Home Location"
case .startLocation: return "Set Start Location"
case .endLocation: return "Set End Location"
}
}
var body: some View {
NavigationStack {
VStack(spacing: 0) {
// Search field
HStack {
Image(systemName: "magnifyingglass")
.foregroundStyle(Theme.textMuted(colorScheme))
.accessibilityHidden(true)
TextField("Search cities, addresses, places...", text: $searchText)
.textFieldStyle(.plain)
.autocorrectionDisabled()
if isSearching {
LoadingSpinner(size: .small)
} else if !searchText.isEmpty {
Button {
searchText = ""
searchResults = []
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(Theme.textMuted(colorScheme))
}
.minimumHitTarget()
.accessibilityLabel("Clear search")
}
}
.padding()
.background(Theme.cardBackgroundElevated(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding()
// Results list
if searchResults.isEmpty && !searchText.isEmpty && !isSearching {
ContentUnavailableView(
"No Results",
systemImage: "mappin.slash",
description: Text("Try a different search term")
)
} else {
List(searchResults) { result in
Button {
onAdd(result.toLocationInput())
dismiss()
} label: {
HStack {
Image(systemName: "mappin.circle.fill")
.foregroundStyle(.red)
.font(.title2)
.accessibilityHidden(true)
VStack(alignment: .leading) {
Text(result.name)
.foregroundStyle(Theme.textPrimary(colorScheme))
if !result.address.isEmpty {
Text(result.address)
.font(.caption)
.foregroundStyle(Theme.textSecondary(colorScheme))
}
}
Spacer()
Image(systemName: "plus.circle")
.foregroundStyle(Theme.warmOrange)
.accessibilityHidden(true)
}
}
.buttonStyle(.plain)
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
}
Spacer()
}
.themedBackground()
.navigationTitle(navigationTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
}
}
.presentationDetents([.large])
.onChange(of: searchText) { _, newValue in
// Debounce search
searchTask?.cancel()
searchTask = Task {
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
await performSearch(query: newValue)
}
}
}
private func performSearch(query: String) async {
guard !query.isEmpty else {
searchResults = []
return
}
isSearching = true
do {
searchResults = try await locationService.searchLocations(query)
} catch {
searchResults = []
}
isSearching = false
}
}
// MARK: - Preview
#Preview {
LocationSearchSheet(inputType: .mustStop) { _ in }
}