|
|
//
|
|
|
// ClubSearchView.swift
|
|
|
// PadelClub
|
|
|
//
|
|
|
// Created by Razmig Sarkissian on 21/03/2024.
|
|
|
//
|
|
|
|
|
|
import SwiftUI
|
|
|
import CoreLocation
|
|
|
import CoreLocationUI
|
|
|
import TipKit
|
|
|
import LeStorage
|
|
|
|
|
|
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
|
|
|
@State private var newClub: Club?
|
|
|
@State private var error: Error?
|
|
|
|
|
|
var presentClubCreationView: Binding<Bool> { Binding(
|
|
|
get: { newClub != nil },
|
|
|
set: { isPresented in
|
|
|
if isPresented == false {
|
|
|
newClub = nil
|
|
|
}
|
|
|
}
|
|
|
)}
|
|
|
|
|
|
var displayContext: DisplayContext = .edition
|
|
|
var club: Club?
|
|
|
var selection: ((Club) -> ())? = nil
|
|
|
|
|
|
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
|
|
|
}
|
|
|
error = nil
|
|
|
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)
|
|
|
self.error = error
|
|
|
Logger.error(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.findOrCreate(name: clubMark.nom, code: clubMark.clubID)
|
|
|
_importClub(clubToEdit: clubToEdit, clubMarker: clubMark)
|
|
|
} label: {
|
|
|
clubView(clubMark)
|
|
|
.contentShape(Rectangle())
|
|
|
}
|
|
|
.frame(maxWidth: .infinity)
|
|
|
.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)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
.task {
|
|
|
do {
|
|
|
try await dataStore.clubs.loadDataFromServerIfAllowed()
|
|
|
} catch {
|
|
|
Logger.error(error)
|
|
|
}
|
|
|
}
|
|
|
.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 {
|
|
|
if error != nil {
|
|
|
Label("Une erreur est survenue", systemImage: "exclamationmark.circle.fill")
|
|
|
} else {
|
|
|
Label("Aucun club trouvé", systemImage: "mappin.slash")
|
|
|
}
|
|
|
} else {
|
|
|
Label("Recherche de club", systemImage: "location.circle")
|
|
|
}
|
|
|
} description: {
|
|
|
if searchAttempted && error != nil {
|
|
|
Text("Tenup est peut-être en maintenance, veuillez ré-essayer plus tard.")
|
|
|
} else {
|
|
|
Text("Padel Club recherche via Tenup 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()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if error != nil {
|
|
|
Link(destination: URLs.tenup.url) {
|
|
|
Text("Voir si tenup est en maintenance")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
RowButtonView("Chercher une ville ou un code postal") {
|
|
|
searchPresented = true
|
|
|
}
|
|
|
|
|
|
if searchAttempted {
|
|
|
RowButtonView("Créer un club manuellement") {
|
|
|
newClub = club ?? Club.newEmptyInstance()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
ContentUnavailableView("recherche en cours", systemImage: "mappin.and.ellipse", description: Text("recherche des clubs autour de vous"))
|
|
|
}
|
|
|
}
|
|
|
.sheet(isPresented: presentClubCreationView) {
|
|
|
if let newClub {
|
|
|
CreateClubView(club: newClub) { club in
|
|
|
selection?(club)
|
|
|
dismiss()
|
|
|
}
|
|
|
.tint(.master)
|
|
|
}
|
|
|
}
|
|
|
.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 _importClub(clubToEdit: Club, clubMarker: ClubMarker) {
|
|
|
if clubToEdit.hasBeenCreated(by: StoreCenter.main.userId) {
|
|
|
if clubToEdit.name.isEmpty {
|
|
|
clubToEdit.name = clubMarker.nom.capitalized
|
|
|
clubToEdit.acronym = clubToEdit.automaticShortName().capitalized
|
|
|
}
|
|
|
clubToEdit.code = clubMarker.clubID
|
|
|
clubToEdit.latitude = clubMarker.lat
|
|
|
clubToEdit.longitude = clubMarker.lng
|
|
|
clubToEdit.city = clubMarker.ville
|
|
|
}
|
|
|
|
|
|
if displayContext == .addition && clubToEdit.hasBeenCreated(by: StoreCenter.main.userId) {
|
|
|
do {
|
|
|
try dataStore.clubs.addOrUpdate(instance: clubToEdit)
|
|
|
} catch {
|
|
|
Logger.error(error)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if dataStore.user.clubs.contains(where: { $0 == clubToEdit.id }) == false {
|
|
|
dataStore.user.clubs.append(clubToEdit.id)
|
|
|
self.dataStore.saveUser()
|
|
|
}
|
|
|
|
|
|
dismiss()
|
|
|
selection?(clubToEdit)
|
|
|
}
|
|
|
|
|
|
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
|
|
|
error = nil
|
|
|
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"
|
|
|
case pickle = "PICKLE"
|
|
|
}
|
|
|
|
|
|
// 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()
|
|
|
//}
|
|
|
|