You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
PadelClub/PadelClub/Views/Club/ClubSearchView.swift

381 lines
14 KiB

//
// ClubSearchView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 21/03/2024.
//
import SwiftUI
import CoreLocation
import CoreLocationUI
import TipKit
struct ClubSearchView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject var dataStore: DataStore
@State private var searchedCity: String = ""
@State private var radius: Double = 50
@State private var clubMarkers : [ClubMarker] = []
@State private var searching: Bool = false
@State private var selectedClubs: [ClubMarker] = []
@State private var searchAttempted: Bool = false
@StateObject var locationManager = LocationManager()
@StateObject private var debouncableViewModel: DebouncableViewModel = DebouncableViewModel()
@State private var getForwardCityList: [CLPlacemark] = []
@State private var searchPresented: Bool = false
@State private var showingSettingsAlert = false
var displayContext: DisplayContext = .edition
var club: Club?
fileprivate class DebouncableViewModel: ObservableObject {
@Published var debouncableText: String = ""
var debounceTrigger: Double = 0.15
}
private var distanceLimit: Measurement<UnitLength> {
Measurement(value: radius, unit: .kilometers)
}
private func _prompt() -> String {
if clubMarkers.isEmpty {
return "Chercher une ville ou un code postal"
} else {
return "Chercher un club parmi ceux listés"
}
}
private func getClubs() async {
do {
defer {
searching = false
searchAttempted = true
}
clubMarkers = []
guard let city = locationManager.city else { return }
let response = try await NetworkFederalService.shared.federalClubs(city: city, radius: radius, location: locationManager.location)
await MainActor.run {
clubMarkers = response.clubMarkers.sorted(by: { a, b in
locationManager.location?.distance(from: a.location) ?? 0 < locationManager.location?.distance(from: b.location) ?? 0
})
}
} catch {
print("getclubs", error)
}
}
/*
Form {
}
.toolbarRole(.editor)
.navigationTitle("Chercher un club")
*/
var body: some View {
List {
if _filteredClubs().isEmpty == false {
Section {
ForEach(_filteredClubs()) { clubMark in
Button {
let clubToEdit = club ?? Club(name: clubMark.nom)
if clubToEdit.name.isEmpty {
clubToEdit.name = clubMark.nom
clubToEdit.acronym = clubToEdit.automaticShortName()
}
clubToEdit.code = clubMark.clubID
clubToEdit.latitude = clubMark.lat
clubToEdit.longitude = clubMark.lng
clubToEdit.city = clubMark.ville
if displayContext == .addition {
try? dataStore.clubs.addOrUpdate(instance: clubToEdit)
}
dismiss()
} label: {
clubView(clubMark)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
} header: {
HStack {
if let city = locationManager.city {
Text(_filteredClubs().count.formatted() + " clubs autour de \(city)")
} else {
Text(_filteredClubs().count.formatted() + " clubs trouvés")
}
Spacer()
Button {
_resetSearch()
} label: {
Text("effacer")
}
.buttonStyle(.borderless)
.textCase(nil)
}
}
}
}
.listStyle(.grouped)
.onChange(of: searchPresented) {
locationManager.lastError = nil
}
.overlay {
if locationManager.requestStarted == false {
if locationManager.lastError != nil {
ContentUnavailableView {
Label("Erreur", systemImage: "exclamationmark.circle")
} description: {
Text("Une erreur est survenue lors de la récupération de votre localisation.")
} actions: {
RowButtonView("D'accord") {
locationManager.lastError = nil
}
}
} else if clubMarkers.isEmpty == false && searching == false && _filteredClubs().isEmpty {
ContentUnavailableView.search(text: searchedCity)
} else if clubMarkers.isEmpty && searching == false && searchPresented == false {
ContentUnavailableView {
if searchAttempted {
Label("Aucun club trouvé", systemImage: "mappin.slash")
} else {
Text("Recherche de club")
}
} description: {
Text("Padel Club peut rechercher un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.")
} actions: {
if locationManager.manager.authorizationStatus != .restricted {
RowButtonView("Chercher autour de moi") {
if locationManager.manager.authorizationStatus == .notDetermined {
locationManager.manager.requestWhenInUseAuthorization()
} else if locationManager.manager.authorizationStatus == .denied {
showingSettingsAlert = true
} else {
locationManager.requestLocation()
}
}
}
RowButtonView("Chercher une ville ou un code postal") {
searchPresented = true
}
}
}
} else {
ContentUnavailableView("recherche en cours", systemImage: "mappin.and.ellipse", description: Text("recherche des clubs autour de vous"))
}
}
.alert(isPresented: $showingSettingsAlert) {
Alert(
title: Text("Réglages"),
message: Text("Pour trouver les clubs autour de vous, vous devez l'autorisation à Padel Club de récupérer votre position."),
primaryButton: .default(Text("Ouvrir les réglages"), action: {
_openSettings()
}),
secondaryButton: .cancel()
)
}
.onReceive(
debouncableViewModel.$debouncableText
.debounce(for: .seconds(debouncableViewModel.debounceTrigger), scheduler: DispatchQueue.main)
) {
guard !$0.isEmpty else {
if searchedCity.isEmpty == false {
searchedCity = ""
}
return
}
print(">> searching for: \($0)")
if debouncableViewModel.debouncableText.trimmed.count > 1 {
searchedCity = $0
}
}
.onChange(of: searchedCity, {
if searchedCity.isEmpty == false {
locationManager.geocodeCity(cityOrZipcode: searchedCity) { cities, error in
getForwardCityList = cities ?? []
}
}
})
.onChange(of: locationManager.requestStarted, { oldValue, newValue in
if oldValue == true && newValue == false {
if locationManager.lastError == nil {
debouncableViewModel.debouncableText = ""
searchedCity = ""
searching = true
getForwardCityList = []
searchPresented = false
Task {
await getClubs()
}
}
}
})
.navigationTitle("Recherche de club")
.searchable(text: $debouncableViewModel.debouncableText, isPresented: $searchPresented, prompt: _prompt())
.autocorrectionDisabled(true)
.keyboardType(.alphabet)
.searchSuggestions {
if clubMarkers.isEmpty {
ForEach(getForwardCityList, id: \.self) { placemark in
Button {
locationManager.location = placemark.location
locationManager.city = placemark._userReadableCityAndZipCode()
} label: {
Text(placemark._userReadableCityAndZipCode())
}
}
}
}
.onSubmit(of: .search, {
if clubMarkers.isEmpty {
debouncableViewModel.debouncableText = ""
searchedCity = ""
searching = true
if getForwardCityList.isEmpty {
locationManager.city = nil
locationManager.location = nil
locationManager.postalCode = nil
} else {
locationManager.location = getForwardCityList.first?.location
locationManager.city = getForwardCityList.first?._userReadableCityAndZipCode()
}
searchPresented = false
Task {
await getClubs()
}
}
})
.toolbarBackground(.visible, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
if locationManager.requestStarted == false && searching == false {
LocationButton(.currentLocation) {
clubMarkers = []
locationManager.requestLocation()
}
.labelStyle(.iconOnly)
.symbolVariant(.fill)
.foregroundColor (Color.white)
.cornerRadius (20)
.font(.system(size: 12))
} else {
ProgressView()
}
}
// if selectedClubs.isEmpty == false {
// Button {
//
// selectedClubs.forEach { club in
//// let federalClub = FederalClubData(context: viewContext)
//// federalClub.updateWith(club)
//// user.addToClubs(federalClub)
// }
//
//// save()
// dismiss()
// } label: {
// Text("Valider")
// }
// }
}
}
private func _filteredClubs() -> [ClubMarker] {
clubMarkers.filter({ _isClubValidForSearchedTerms(club: $0) })
}
private func _isClubValidForSearchedTerms(club: ClubMarker) -> Bool {
searchedCity.isEmpty ||
club.nom.localizedCaseInsensitiveContains(searchedCity) ||
club.ville.localizedCaseInsensitiveContains(searchedCity)
}
private func _resetSearch() {
searchAttempted = false
debouncableViewModel.debouncableText = ""
searchedCity = ""
locationManager.city = nil
locationManager.location = nil
locationManager.postalCode = nil
locationManager.lastError = nil
clubMarkers = []
getForwardCityList = []
}
private func _openSettings() {
guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else {
return
}
UIApplication.shared.open(settingsURL)
}
@ViewBuilder
private func clubView(_ club: ClubMarker) -> some View {
LabeledContent {
Text(club.distance(from: locationManager.location))
} label: {
Text(club.nom)
Text(club.ville).font(.caption)
}
}
}
// MARK: - FederalClubResponse
struct FederalClubResponse: Codable {
let typeRecherche: String
let nombreResultat: Int
let clubMarkers: [ClubMarker]
enum CodingKeys: String, CodingKey {
case typeRecherche, nombreResultat
case clubMarkers = "club_markers"
}
}
enum Pratique: String, Codable {
case beach = "BEACH"
case padel = "PADEL"
case tennis = "TENNIS"
}
// MARK: - ClubMarker
struct ClubMarker: Codable, Hashable, Identifiable {
let nom, clubID, ville, distance: String
let terrainPratiqueLibelle: String
let pratiques: [Pratique]
let lat, lng: Double
var location: CLLocation {
CLLocation(latitude: lat, longitude: lng)
}
func distance(from location: CLLocation?) -> String {
guard let location else { return "" }
let measurement = Measurement(value: location.distance(from: self.location) / 1000, unit: UnitLength.kilometers)
return measurement.formatted()
}
var id: String {
clubID
}
enum CodingKeys: String, CodingKey {
case nom
case clubID = "clubId"
case ville, distance, terrainPratiqueLibelle, pratiques, lat, lng
}
}
fileprivate extension CLPlacemark {
func _userReadableCityAndZipCode() -> String {
[locality, postalCode].compactMap { $0 }.joined(separator: ", ")
}
}
#Preview {
ClubSearchView()
}