- Delete TripCreationView.swift and TripCreationViewModel.swift (unused) - Extract TripOptionsView to standalone file - Extract DateRangePicker and DayCell to standalone file - Extract LocationSearchSheet and CityInputType to standalone file - Fix TripWizardView to pass games dictionary to TripOptionsView - Remove debug print statements from TripDetailView Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
142 lines
4.7 KiB
Swift
142 lines
4.7 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
|
|
@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
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(spacing: 0) {
|
|
// Search field
|
|
HStack {
|
|
Image(systemName: "magnifyingglass")
|
|
.foregroundStyle(.secondary)
|
|
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(.secondary)
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.secondarySystemBackground))
|
|
.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)
|
|
VStack(alignment: .leading) {
|
|
Text(result.name)
|
|
.foregroundStyle(.primary)
|
|
if !result.address.isEmpty {
|
|
Text(result.address)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
Spacer()
|
|
Image(systemName: "plus.circle")
|
|
.foregroundStyle(.blue)
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
.listStyle(.plain)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.navigationTitle(inputType == .mustStop ? "Add Must-Stop" : "Add Location")
|
|
.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 }
|
|
}
|