From 1ae17aa65322bdda91ecc516d56055a43dce6cea Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Thu, 21 Mar 2024 19:12:32 +0100 Subject: [PATCH] add club management --- PadelClub.xcodeproj/project.pbxproj | 50 +++ PadelClub/Data/Club.swift | 72 +++- PadelClub/Data/DataStore.swift | 2 +- PadelClub/Data/Migration/ClubV1.swift | 2 +- PadelClub/Data/MockData.swift | 16 + PadelClub/Extensions/Array+Extensions.swift | 12 + PadelClub/Manager/DisplayContext.swift | 13 + PadelClub/Manager/LocationManager.swift | 67 +++ .../Network/NetworkFederalService.swift | 81 ++++ PadelClub/Manager/Tips.swift | 330 +++++++++++++++ PadelClub/PadelClubApp.swift | 7 + PadelClub/Views/Club/ClubDetailView.swift | 153 +++++++ PadelClub/Views/Club/ClubImportView.swift | 43 ++ PadelClub/Views/Club/ClubSearchView.swift | 381 ++++++++++++++++++ PadelClub/Views/Club/ClubsView.swift | 94 +++++ PadelClub/Views/Club/CreateClubView.swift | 46 +++ PadelClub/Views/ClubView.swift | 2 +- .../Views/Components/RowButtonView.swift | 34 +- PadelClub/Views/ContentView.swift | 2 +- .../Views/Navigation/Umpire/UmpireView.swift | 8 + 20 files changed, 1395 insertions(+), 20 deletions(-) create mode 100644 PadelClub/Data/MockData.swift create mode 100644 PadelClub/Manager/DisplayContext.swift create mode 100644 PadelClub/Manager/LocationManager.swift create mode 100644 PadelClub/Manager/Network/NetworkFederalService.swift create mode 100644 PadelClub/Manager/Tips.swift create mode 100644 PadelClub/Views/Club/ClubDetailView.swift create mode 100644 PadelClub/Views/Club/ClubImportView.swift create mode 100644 PadelClub/Views/Club/ClubSearchView.swift create mode 100644 PadelClub/Views/Club/ClubsView.swift create mode 100644 PadelClub/Views/Club/CreateClubView.swift diff --git a/PadelClub.xcodeproj/project.pbxproj b/PadelClub.xcodeproj/project.pbxproj index d1dc787..707187f 100644 --- a/PadelClub.xcodeproj/project.pbxproj +++ b/PadelClub.xcodeproj/project.pbxproj @@ -32,6 +32,12 @@ C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAC2B85FCCD00ADC637 /* User.swift */; }; C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB02B86375E00ADC637 /* MainUserView.swift */; }; C4A47DB32B86387500ADC637 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DB22B86387500ADC637 /* AccountView.swift */; }; + FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5502BAB351300FD8220 /* ClubDetailView.swift */; }; + FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5522BAB354A00FD8220 /* MockData.swift */; }; + FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5542BAB36DD00FD8220 /* CreateClubView.swift */; }; + FF1DC5572BAB3AED00FD8220 /* ClubsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5562BAB3AED00FD8220 /* ClubsView.swift */; }; + FF1DC5592BAB767000FD8220 /* Tips.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC5582BAB767000FD8220 /* Tips.swift */; }; + FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */; }; FF2BE4872B85E27400592328 /* LeStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C425D4542B6D24E2002A7B48 /* LeStorage.framework */; }; FF2BE4882B85E27400592328 /* LeStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C425D4542B6D24E2002A7B48 /* LeStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; }; @@ -61,6 +67,10 @@ FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */; }; FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */; }; FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */; }; + FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */; }; + FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; }; + FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; }; + FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */; }; FFD783FD2B91B9ED000F62A6 /* AgendaDestinationPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FC2B91B9ED000F62A6 /* AgendaDestinationPickerView.swift */; }; FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */; }; FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD784012B91C1B4000F62A6 /* WelcomeView.swift */; }; @@ -149,6 +159,12 @@ C4A47DAC2B85FCCD00ADC637 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; C4A47DB02B86375E00ADC637 /* MainUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUserView.swift; sourceTree = ""; }; C4A47DB22B86387500ADC637 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = ""; }; + FF1DC5502BAB351300FD8220 /* ClubDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubDetailView.swift; sourceTree = ""; }; + FF1DC5522BAB354A00FD8220 /* MockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockData.swift; sourceTree = ""; }; + FF1DC5542BAB36DD00FD8220 /* CreateClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateClubView.swift; sourceTree = ""; }; + FF1DC5562BAB3AED00FD8220 /* ClubsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubsView.swift; sourceTree = ""; }; + FF1DC5582BAB767000FD8220 /* Tips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tips.swift; sourceTree = ""; }; + FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayContext.swift; sourceTree = ""; }; FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; FF3F74F52B919E45004CFE0E /* UmpireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireView.swift; sourceTree = ""; }; @@ -176,6 +192,10 @@ FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InscriptionManagerView.swift; sourceTree = ""; }; FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizedTournamentView.swift; sourceTree = ""; }; FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; + FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = ""; }; + FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = ""; }; + FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = ""; }; + FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubImportView.swift; sourceTree = ""; }; FFD783FC2B91B9ED000F62A6 /* AgendaDestinationPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaDestinationPickerView.swift; sourceTree = ""; }; FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubView.swift; sourceTree = ""; }; FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; @@ -302,6 +322,7 @@ C4A47D5D2B6D38EC00ADC637 /* DataStore.swift */, C4A47D592B6D383C00ADC637 /* Tournament.swift */, C4A47D622B6D3D6500ADC637 /* Club.swift */, + FF1DC5522BAB354A00FD8220 /* MockData.swift */, FF6EC9012B94799200EA7F5A /* Coredata */, FF6EC9022B9479B900EA7F5A /* Federal */, ); @@ -314,6 +335,7 @@ C425D4022B6D249D002A7B48 /* ContentView.swift */, C4A47D732B72881F00ADC637 /* ClubView.swift */, FF39719B2B8DE04B004C4E75 /* Navigation */, + FF1DC54D2BAB34FA00FD8220 /* Club */, FF3F74F72B919F96004CFE0E /* Tournament */, C4A47D882B7BBB5000ADC637 /* Subscription */, C4A47D852B7BA33F00ADC637 /* User */, @@ -367,6 +389,18 @@ path = Components; sourceTree = ""; }; + FF1DC54D2BAB34FA00FD8220 /* Club */ = { + isa = PBXGroup; + children = ( + FF1DC5502BAB351300FD8220 /* ClubDetailView.swift */, + FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */, + FF1DC5562BAB3AED00FD8220 /* ClubsView.swift */, + FF1DC5542BAB36DD00FD8220 /* CreateClubView.swift */, + FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */, + ); + path = Club; + sourceTree = ""; + }; FF39719B2B8DE04B004C4E75 /* Navigation */ = { isa = PBXGroup; children = ( @@ -475,6 +509,7 @@ isa = PBXGroup; children = ( FF4AB6B42B9248200002987F /* NetworkManager.swift */, + FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */, FF6EC9052B947A1000EA7F5A /* NetworkManagerError.swift */, ); path = Network; @@ -503,8 +538,11 @@ FFF8ACD02B9238A2008466FA /* Manager */ = { isa = PBXGroup; children = ( + FF1DC5582BAB767000FD8220 /* Tips.swift */, + FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */, FFF8ACD12B9238C3008466FA /* FileImportManager.swift */, FFF8ACD32B92392C008466FA /* SourceFileManager.swift */, + FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */, FF6EC9072B947A1E00EA7F5A /* Network */, ); path = Manager; @@ -681,6 +719,7 @@ FF7091662B90F0B000AB08DA /* TabDestination.swift in Sources */, C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */, C4A47D8A2B7BBB6500ADC637 /* SubscriptionView.swift in Sources */, + FF1DC5572BAB3AED00FD8220 /* ClubsView.swift in Sources */, FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */, C4A47DB12B86375E00ADC637 /* MainUserView.swift in Sources */, FF7091682B90F79F00AB08DA /* TournamentCellView.swift in Sources */, @@ -690,7 +729,10 @@ FF6EC9002B94794700EA7F5A /* PresentationContext.swift in Sources */, C4A47DA92B85F82100ADC637 /* ChangePasswordView.swift in Sources */, FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */, + FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */, FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */, + FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */, + FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */, FF7091622B90F04300AB08DA /* TournamentOrganizerView.swift in Sources */, C4A47D742B72881F00ADC637 /* ClubView.swift in Sources */, C4A47D902B7BBBEC00ADC637 /* StoreManager.swift in Sources */, @@ -704,9 +746,11 @@ FF82CFC52B911F5B00B0CAF2 /* OrganizedTournamentView.swift in Sources */, FF59FFB32B90EFAC0061EFF9 /* EventListView.swift in Sources */, C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */, + FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */, FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */, FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */, FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */, + FF1DC5512BAB351300FD8220 /* ClubDetailView.swift in Sources */, C4A47D632B6D3D6500ADC637 /* Club.swift in Sources */, FF6EC90B2B947AC000EA7F5A /* Array+Extensions.swift in Sources */, FF59FFB92B90EFD70061EFF9 /* ToolboxView.swift in Sources */, @@ -714,8 +758,10 @@ FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */, FFF8ACCD2B92367B008466FA /* FederalPlayer.swift in Sources */, FF6EC9092B947A5300EA7F5A /* FixedWidthInteger+Extensions.swift in Sources */, + FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */, FF70916E2B9108C600AB08DA /* InscriptionManagerView.swift in Sources */, FF82CFC92B9132AF00B0CAF2 /* ActivityView.swift in Sources */, + FF1DC55B2BAB80C400FD8220 /* DisplayContext.swift in Sources */, C425D4032B6D249D002A7B48 /* ContentView.swift in Sources */, FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */, C425D4012B6D249D002A7B48 /* PadelClubApp.swift in Sources */, @@ -725,7 +771,9 @@ C4A47D772B73789100ADC637 /* TournamentV1.swift in Sources */, C4A47DAD2B85FCCD00ADC637 /* User.swift in Sources */, FFF8ACD22B9238C3008466FA /* FileImportManager.swift in Sources */, + FF1DC5532BAB354A00FD8220 /* MockData.swift in Sources */, FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */, + FF1DC5592BAB767000FD8220 /* Tips.swift in Sources */, FF59FFB72B90EFBF0061EFF9 /* MainView.swift in Sources */, FFD784022B91C1B4000F62A6 /* WelcomeView.swift in Sources */, FFF8ACD62B923960008466FA /* URL+Extensions.swift in Sources */, @@ -901,6 +949,7 @@ DEVELOPMENT_TEAM = 526E96RFNP; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; @@ -930,6 +979,7 @@ DEVELOPMENT_TEAM = 526E96RFNP; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Padel Club a besoin de votre position pour rechercher les clubs autour de vous."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; diff --git a/PadelClub/Data/Club.swift b/PadelClub/Data/Club.swift index dd9298a..46616a3 100644 --- a/PadelClub/Data/Club.swift +++ b/PadelClub/Data/Club.swift @@ -6,19 +6,43 @@ // import Foundation +import SwiftUI import LeStorage -class Club : ModelObject, Storable { +@Observable +class Club : ModelObject, Storable, Hashable { static func resourceName() -> String { return "clubs" } + static func == (lhs: Club, rhs: Club) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + return hasher.combine(id) + } + var id: String = Store.randomId() var name: String - var address: String - - init(name: String, address: String) { + var acronym: String + var phone: String? + var code: String? + var address: String? + var city: String? + var zipCode: String? + var latitude: Double? + var longitude: Double? + + internal init(name: String, acronym: String? = nil, phone: String? = nil, code: String? = nil, address: String? = nil, city: String? = nil, zipCode: String? = nil, latitude: Double? = nil, longitude: Double? = nil) { self.name = name + self.acronym = acronym ?? name.canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines) + self.phone = phone + self.code = code self.address = address + self.city = city + self.zipCode = zipCode + self.latitude = latitude + self.longitude = longitude } var tournaments: [Tournament] { @@ -28,5 +52,45 @@ class Club : ModelObject, Storable { override func deleteDependencies() throws { try Store.main.deleteDependencies(items: self.tournaments) } + + enum CodingKeys: String, CodingKey { + case _id = "id" + case _name = "name" + case _acronym = "acronym" + case _phone = "phone" + case _code = "code" + case _address = "address" + case _city = "city" + case _zipCode = "zipCode" + case _latitude = "latitude" + case _longitude = "longitude" + } +} + +extension Club { + var isValid: Bool { + name.isEmpty == false && acronym.isEmpty == false + } + func automaticShortName() -> String { + name.canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines) + } + + enum AcronymMode: String, CaseIterable { + case automatic = "Automatique" + case custom = "Personalisée" + } + + func shortNameMode() -> AcronymMode { + (acronym.isEmpty || acronym == automaticShortName()) ? .automatic : .custom + } + + func hasTenupId() -> Bool { + code != nil + } + + func federalLink() -> URL? { + guard let code else { return nil } + return URL(string: "https://tenup.fft.fr/club/\(code)") + } } diff --git a/PadelClub/Data/DataStore.swift b/PadelClub/Data/DataStore.swift index f89249f..0314a9e 100644 --- a/PadelClub/Data/DataStore.swift +++ b/PadelClub/Data/DataStore.swift @@ -45,7 +45,7 @@ class DataStore: ObservableObject { // store.addMigration(Migration(version: 2)) // store.addMigration(Migration(version: 3)) - self.clubs = store.registerCollection(synchronized: true) + self.clubs = store.registerCollection(synchronized: false) self.tournaments = store.registerCollection(synchronized: false) NotificationCenter.default.addObserver(self, selector: #selector(collectionWasUpdated), name: NSNotification.Name.CollectionDidLoad, object: nil) diff --git a/PadelClub/Data/Migration/ClubV1.swift b/PadelClub/Data/Migration/ClubV1.swift index 57343c4..f73af42 100644 --- a/PadelClub/Data/Migration/ClubV1.swift +++ b/PadelClub/Data/Migration/ClubV1.swift @@ -32,7 +32,7 @@ class ClubV1 : ModelObject, Storable, MigrationSource { typealias Destination = Club func migrate() -> Club { - return Club(name: self.name, address: "3 impasse des chevreuils") + return Club(name: self.name, acronym: "test", address: "3 impasse des chevreuils") // return Club(name: self.name, address: "3 impasse des chevreuils") } diff --git a/PadelClub/Data/MockData.swift b/PadelClub/Data/MockData.swift new file mode 100644 index 0000000..0df8d6e --- /dev/null +++ b/PadelClub/Data/MockData.swift @@ -0,0 +1,16 @@ +// +// MockData.swift +// PadelClub +// +// Created by Razmig Sarkissian on 20/03/2024. +// + +extension Club { + static func mock() -> Club { + Club(name: "AUC", acronym: "AUC") + } + + static func newEmptyInstance() -> Club { + Club(name: "", acronym: "") + } +} diff --git a/PadelClub/Extensions/Array+Extensions.swift b/PadelClub/Extensions/Array+Extensions.swift index baa4ea6..d908a0c 100644 --- a/PadelClub/Extensions/Array+Extensions.swift +++ b/PadelClub/Extensions/Array+Extensions.swift @@ -18,3 +18,15 @@ extension Array { return !self.allSatisfy { !p($0) } } } + +extension Array where Element: Equatable { + + /// Remove first collection element that is equal to the given `object` or `element`: + mutating func remove(elements: [Element]) { + elements.forEach { + if let index = firstIndex(of: $0) { + remove(at: index) + } + } + } +} diff --git a/PadelClub/Manager/DisplayContext.swift b/PadelClub/Manager/DisplayContext.swift new file mode 100644 index 0000000..2517536 --- /dev/null +++ b/PadelClub/Manager/DisplayContext.swift @@ -0,0 +1,13 @@ +// +// DisplayContext.swift +// PadelClub +// +// Created by Razmig Sarkissian on 20/03/2024. +// + +import Foundation + +enum DisplayContext { + case addition + case edition +} diff --git a/PadelClub/Manager/LocationManager.swift b/PadelClub/Manager/LocationManager.swift new file mode 100644 index 0000000..660f095 --- /dev/null +++ b/PadelClub/Manager/LocationManager.swift @@ -0,0 +1,67 @@ +// +// LocationManager.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 02/09/2023. +// + +import Foundation +import CoreLocation + +class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { + let manager = CLLocationManager() + + @Published var location: CLLocation? + @Published var city: String? + @Published var postalCode: String? + @Published var requestStarted: Bool = false + @Published var userReadableCityOrZipcode: String = "" + @Published var lastError: Error? = nil + var shouldRequestLocation: Bool = false + + override init() { + super.init() + manager.delegate = self + } + + func requestLocation() { + lastError = nil + manager.requestLocation() + requestStarted = true + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + location = locations.first + location?.geocode(completion: { placemark, error in + self.city = placemark?.first?.locality + self.postalCode = placemark?.first?.postalCode + self.requestStarted = false + }) + } + + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + if manager.authorizationStatus == .authorizedWhenInUse || manager.authorizationStatus == .authorizedAlways { + if requestStarted == false && shouldRequestLocation { + DispatchQueue.main.async { + self.requestLocation() + } + } + } + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + print("locationManager didFailWithError", error) + requestStarted = false + self.lastError = error + } + + func geocodeCity(cityOrZipcode: String, completion: @escaping (_ placemark: [CLPlacemark]?, _ error: Error?) -> Void) { + CLGeocoder().geocodeAddressString(cityOrZipcode, in: nil, completionHandler: completion) + } +} + +extension CLLocation { + func geocode(completion: @escaping (_ placemark: [CLPlacemark]?, _ error: Error?) -> Void) { + CLGeocoder().reverseGeocodeLocation(self, completionHandler: completion) + } +} diff --git a/PadelClub/Manager/Network/NetworkFederalService.swift b/PadelClub/Manager/Network/NetworkFederalService.swift new file mode 100644 index 0000000..5a8e910 --- /dev/null +++ b/PadelClub/Manager/Network/NetworkFederalService.swift @@ -0,0 +1,81 @@ +// +// NetworkFederalService.swift +// PadelClub +// +// Created by Razmig Sarkissian on 21/03/2024. +// + +import Foundation +import CoreLocation + +class NetworkFederalService { + static let shared: NetworkFederalService = NetworkFederalService() + var formId = "" + + var tenupJsonDecoder: JSONDecoder = { + let decoder = JSONDecoder() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" + decoder.dateDecodingStrategy = .formatted(dateFormatter) + return decoder + }() + + func runTenupTask(request: URLRequest) async throws -> T { + let task = try await URLSession.shared.data(for: request) + if request.httpMethod == "PUT" { + print("tried PUT: \(request.url!)") + if let urlResponse = task.1 as? HTTPURLResponse { + print(urlResponse.statusCode) + } + } + return try tenupJsonDecoder.decode(T.self, from: task.0) + } + + func federalClubs(country: String = "fr", city: String, radius: Double, location: CLLocation? = nil) async throws -> FederalClubResponse { + + /* + { + "geocoding[country]": "fr", + "geocoding[ville]": "Cayenne, 973, Guyane", + "geocoding[rayon]": "15", + "geocoding[userPosition][lng]": "-52.311583", + "geocoding[userPosition][lat]": "4.925248", + "geocoding[userPosition][showDistance]": "false", + "pratiqueOption[0]": "PADEL", + "nombreResultat": "6", + "diplomeEtatOption": "false", + "galaxieOption": "false", + "fauteuilOption": "false", + "tennisSanteOption": "false" + } + */ + + var parameters = "geocoding[country]=\(country)&geocoding[ville]=\(city)&geocoding[rayon]=\(Int(radius))&pratiqueOption[0]=Padel" + + if let location { + parameters = parameters + "&geocoding[userPosition][lat]=\(location.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))))&geocoding[userPosition][lng]=\(location.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))))&geocoding[userPosition][showDistance]=true" + } + //"geocoding%5Bcountry%5D=fr&geocoding%5Bville%5D=13%20Avenue%20Emile%20Bodin%2013260%20Cassis&geocoding%5Brayon%5D=15&geocoding%5BuserPosition%5D%5Blat%5D=43.22278594081477&geocoding%5BuserPosition%5D%5Blng%5D=5.556953900769194&geocoding%5BuserPosition%5D%5BshowDistance%5D=true&nombreResultat=0&diplomeEtatOption=false&galaxieOption=false&fauteuilOption=false&tennisSanteOption=false" + let postData = parameters.data(using: .utf8) + + var request = URLRequest(url: URL(string: "https://tenup.fft.fr/recherche/clubs/ajax")!,timeoutInterval: Double.infinity) + request.addValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept") + request.addValue("fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", forHTTPHeaderField: "Accept-Language") + request.addValue("gzip, deflate, br", forHTTPHeaderField: "Accept-Encoding") + request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.addValue("https://tenup.fft.fr", forHTTPHeaderField: "Origin") + request.addValue("keep-alive", forHTTPHeaderField: "Connection") + request.addValue("https://tenup.fft.fr/recherche/clubs/list", forHTTPHeaderField: "Referer") + // request.addValue("a20ba3b563e5ce7ad731c2c1076b217f=a2de91fbefddf75ea4aa86297ed09bd5; visid_incap_2712217=TZgb6G1zTsiPtpJ4cCmOErtj8GQAAAAAQUIPAAAAAAC5nrgD+rm7QWCdUN5I8Y6T; nlbi_2712217=Ug01X5TrSizGkQw5qBb2twAAAAAOvBNMkIHMeRAJGDiOaFxs; incap_ses_391_2712217=E60LNJzW7B+BjW0qWx1tBbtj8GQAAAAAlw5keZVI9C7egwKQblAHeQ==; TCPID=1238411561211442193459; TCID=; incap_ses_391_2712217=MzHHL4jK9k4gpmkqWx1tBT9i8GQAAAAA1k2Eroyuow6SC5Zmf1WtVA==; visid_incap_2712217=lVlg9romTq6I9k4sVklsgr9F72QAAAAAQUIPAAAAAADw7ISp7aFXSsqidxqlj3Df", forHTTPHeaderField: "Cookie") + request.addValue("empty", forHTTPHeaderField: "Sec-Fetch-Dest") + request.addValue("cors", forHTTPHeaderField: "Sec-Fetch-Mode") + request.addValue("same-origin", forHTTPHeaderField: "Sec-Fetch-Site") + request.addValue("trailers", forHTTPHeaderField: "TE") + + request.httpMethod = "POST" + request.httpBody = postData + + return try await runTenupTask(request: request) + } + +} diff --git a/PadelClub/Manager/Tips.swift b/PadelClub/Manager/Tips.swift new file mode 100644 index 0000000..a9c1c2c --- /dev/null +++ b/PadelClub/Manager/Tips.swift @@ -0,0 +1,330 @@ +// +// Tips.swift +// Padel Tournament +// +// Created by Razmig Sarkissian on 18/12/2023. +// + +import Foundation +import TipKit + +struct PadelBeachExportTip: Tip { + var title: Text { + Text("Inscrire les équipes sur le site fédéral") + } + + + var message: Text? { + Text("Allez sur beach-padel.app.fft.fr pour y inscrire les paires que vous avez préparé.") + } + + var image: Image? { + Image(systemName: "square.and.arrow.up") + } + + var actions: [Action] { + Action(id: "more-info-export", title: "En savoir plus") + Action(id: "beach-padel", title: "beach-padel.app.fft.fr") + } + +} + +struct PadelBeachImportTip: Tip { + var title: Text { + Text("Importer les paires du site fédéral") + } + + + var message: Text? { + Text("Allez sur beach-padel.app.fft.fr pour exporter les paires que vous avez inscrites, puis importer le fichier dans Padel Club") + } + + var image: Image? { + Image(systemName: "square.and.arrow.down") + } + + var actions: [Action] { + Action(id: "more-info-import", title: "Importer le fichier excel beach-padel") + } +} + +struct GenerateLoserBracketTip: Tip { + var title: Text { + Text("Générer les matchs de classements") + } + + + var message: Text? { + Text("Si vous êtes satisfait de votre tableau, vous pouvez générer les matchs de classements pour faciliter la gestion de votre programmation. L'option est disponible dans le menu en haut à droite.") + } + + var image: Image? { + nil + } + + + var actions: [Action] { + Action(id: "generate-loser-bracket", title: "Générer les matchs de classements") + } + +} + + +struct TeamChampionshipTip: Tip { + var title: Text { + Text("Gérer vos rencontres du championnat par équipe") + } + + + var message: Text? { + Text("Padel Club vous permet de gérer vos équipes, préparer vos paires pour les rencontres, calculer leurs poids, vérifier la validité des jokers, noter les scores et diffuser les résultats.") + } + + var image: Image? { + Image(systemName: "person.3") + } + + var actions: [Action] { + Action(id: "list-manager", title: "Ouvrir le gestionnaire d'équipe") + } + +} + + +struct TeamChampionshipMainScreenTip: Tip { + var title: Text { + Text("Affichage sur l'écran principal") + } + + + var message: Text? { + Text("Afficher l'accès au gestionnaire d'équipe sur l'écran principal. Vous pourrez le modifier plus tard dans les options en haut à droite de cet écran.") + } + + var image: Image? { + Image(systemName: "arrow.uturn.backward") + } + + var actions: [Action] { + Action(id: "set-list-manager-main", title: "Afficher sur l'écran principal") + } + +} + +struct InscriptionManagerPasteInputTip: Tip { + var title: Text { + Text("Copier / Coller") + } + + + var message: Text? { + Text("Copiez les messages d'inscriptions que vous recevez, que ce soit SMS, email, WhatsApp ou autre, puis utilisez le bouton pour le coller, Padel Club se chargera de vous proposer les joueurs détectés dans le message.") + } + + var image: Image? { + Image(systemName: "doc.on.clipboard") + } + + var actions: [Action] { + Action(id: "add-team-paste", title: "Collez le contenu du presse-papier") + } +} + +struct InscriptionManagerSearchInputTip: Tip { + var title: Text { + Text("Rechercher dans la base fédérale") + } + + + var message: Text? { + Text("Padel Club contient la base fédérale public de tous les joueurs ayant déjà participé à au moins un tournoi. Ajouter rapidement une équipe grâce à cette fonction.") + } + + var image: Image? { + Image(systemName: "magnifyingglass") + } + + var actions: [Action] { + Action(id: "add-team-search", title: "Chercher dans la base fédérale") + } +} + + +struct InscriptionManagerCreateInputTip: Tip { + var title: Text { + Text("Créer un joueur manuellement") + } + + + var message: Text? { + Text("Si le joueur est introuvable, cela indique qu'il n'a jamais fait de compétition, rajoutez-le rapidement manuellement. Padel Club calcul le rang du non-classé ce mois-ci automatiquement.") + } + + var image: Image? { + Image(systemName: "person.badge.plus") + } + + var actions: [Action] { + Action(id: "add-team-create", title: "Créer un ou une joueuse") + } +} + +struct InscriptionManagerFileInputTip: Tip { + var title: Text { + Text("Importer le fichier beach-padel.app.fft.fr") + } + + + var message: Text? { + Text("Padel Club vous permet d'importer le fichier excel fourni par beach-padel.app.fft.fr") + } + + var image: Image? { + Image(systemName: "doc") + } + + var actions: [Action] { + Action(id: "add-team-file", title: "Choisir un fichier") + Action(id: "website", title: "Aller sur beach-padel.app.fft.fr") + } +} + +struct InscriptionManagerWomanRankTip: Tip { + var title: Text { + Text("Rang d'une joueuse dans un tournoi messieurs") + } + + + var message: Text? { + Text("Padel Club calcul automatiquement le rang d'une joueuse inscrite dans un tournoi messieurs.") + } +} + +struct InscriptionManagerRankUpdateTip: Tip { + var title: Text { + Text("Nouveau classement disponible") + } + + + var message: Text? { + Text("Padel Club vous permet de mettre à jour le classement des équipes inscrites. Si vous avez clôturé les inscriptions, la mise à jour du classement ne modifie pas la phase d'intégration de l'équipe, poule ou tableau final. Vous pouvez manuellement mettre à jour cette option.") + } + + var image: Image? { + Image(systemName: "list.number") + } + + var actions: [Action] { + Action(id: "update-rank", title: "Mettre à jour les classements") + } + +} + +struct SharePictureTip: Tip { + var title: Text { + Text("Partage d'un match avec une photo") + } + + var message: Text? { + Text("Lors d'un partage d'une photo, le texte est disponible dans le presse-papier du téléphone") + } + + var image: Image? { + Image(systemName: "photo.badge.checkmark.fill") + } +} + +struct NewRankDataAvailableTip: Tip { + var title: Text { + Text("Nouveau classement disponible") + } + + var message: Text? { + Text("Padel Club récupère toutes les données publique provenant de la FFT. L'importation de ce nouveau classement peut prendre plusieurs dizaines de secondes.") + } + + var image: Image? { + Image(systemName: "exclamationmark.icloud") + } + + var actions: [Action] { + //Action(id: "show-rank", title: Padel_TournamentApp.padelRankingWebsite.absoluteString) + Action(id: "update-rank", title: "Démarrer l'importation") + } + +} + +struct ClubSearchTip: Tip { + var title: Text { + Text("Recherche d'un club") + } + + var message: Text? { + Text("Padel Club peut rechercher un club autour de vous, d'une ville ou d'un code postal, facilitant ainsi la saisie d'information.") + } + + var image: Image? { + Image(systemName: "house.and.flag.fill") + } + + var actions: [Action] { + Action(id: ActionKey.searchAroundMe.rawValue, title: "Chercher autour de moi") + Action(id: ActionKey.searchCity.rawValue, title: "Chercher une ville") + } + + enum ActionKey: String { + case searchAroundMe = "search-around-me" + case searchCity = "search-city" + + } +} + +struct SlideToDeleteTip: Tip { + var title: Text { + Text("Glisser pour effacer") + } + + var message: Text? { + Text("Vous pouvez effacer un club en glissant votre doigt vers la gauche") + } + + var image: Image? { + Image(systemName: "trash") + } + +} + +struct TipStyleModifier: ViewModifier { + @Environment(\.colorScheme) var colorScheme + var tint: Color? + + func body(content: Content) -> some View { + if let tint { + if colorScheme == .light { + content + .tint(tint) + .listRowInsets(EdgeInsets()) + .tipBackground(.white) + } else { + content + .tint(tint) + .listRowInsets(EdgeInsets()) + } + } else { + if colorScheme == .light { + content + .listRowInsets(EdgeInsets()) + .tipBackground(.white) + } else { + content + .listRowInsets(EdgeInsets()) + } + } + } +} + +extension View { + func tipStyle(tint: Color?) -> some View { + modifier(TipStyleModifier(tint: tint)) + } +} diff --git a/PadelClub/PadelClubApp.swift b/PadelClub/PadelClubApp.swift index 023f73d..5f7d9c2 100644 --- a/PadelClub/PadelClubApp.swift +++ b/PadelClub/PadelClubApp.swift @@ -7,6 +7,7 @@ import SwiftUI import LeStorage +import TipKit @main struct PadelClubApp: App { @@ -18,6 +19,12 @@ struct PadelClubApp: App { .onAppear { self._onAppear() } + .task { + try? Tips.configure([ + .displayFrequency(.immediate), + .datastoreLocation(.applicationDefault) + ]) + } .environment(\.managedObjectContext, persistenceController.localContainer.viewContext) } } diff --git a/PadelClub/Views/Club/ClubDetailView.swift b/PadelClub/Views/Club/ClubDetailView.swift new file mode 100644 index 0000000..45ddf87 --- /dev/null +++ b/PadelClub/Views/Club/ClubDetailView.swift @@ -0,0 +1,153 @@ +// +// ClubDetailView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 20/03/2024. +// + +import SwiftUI + +struct ClubDetailView: View { + @Bindable var club: Club + var displayContext: DisplayContext + @EnvironmentObject var dataStore: DataStore + @FocusState var focusedField: Club.CodingKeys? + @State private var acronymMode: Club.AcronymMode = .automatic + @State private var updateClubData: Bool = false + + init(club: Club, displayContext: DisplayContext = .edition) { + _club = Bindable(club) + self.displayContext = displayContext + _acronymMode = State(wrappedValue: club.shortNameMode()) + } + + var body: some View { + Form { + Section { + VStack(alignment: .leading, spacing: 0) { + Text("Nom du club").foregroundStyle(.secondary).font(.caption) + TextField("Nom du club", text: $club.name) + .fixedSize() + .focused($focusedField, equals: ._name) + .submitLabel( displayContext == .addition ? .next : .done) + .onSubmit { + if club.acronym.isEmpty { + club.acronym = club.name.canonicalVersion.replaceCharactersFromSet(characterSet: .whitespacesAndNewlines) + } + if displayContext == .addition { + focusedField = ._acronym + } + } + } + .onTapGesture { + focusedField = ._name + } + LabeledContent { + if acronymMode == .automatic { + Text(club.acronym) + } else { + TextField("Nom court", text: $club.acronym) + .textInputAutocapitalization(.never) + .fixedSize() + .focused($focusedField, equals: ._acronym) + .submitLabel(.done) + .multilineTextAlignment(.trailing) + } + } label: { + VStack(alignment: .leading, spacing: 0) { + Text("Nom court").foregroundStyle(.secondary).font(.caption) + Menu { + Section { + ForEach(Club.AcronymMode.allCases, id: \.self) { option in + Toggle(isOn: .init(get: { + acronymMode == option + }, set: { value in + acronymMode = option + })) { + Text(option.rawValue) + } + } + } header: { + Text("Nom court") + } + } label: { + Text(acronymMode.rawValue) + } + } + } + .onChange(of: acronymMode) { + focusedField = ._acronym + if acronymMode == .custom { + club.acronym = "" + } + } + } footer: { + Text("Vous pouvez personaliser le nom court ou laisser celui généré par défaut. Le nom court est utile au niveau des liens de diffusions.") + } + + + if club.code == nil || updateClubData { + Section { + NavigationLink { + ClubSearchView(displayContext: .edition, club: club) + } label: { + Label("Chercher dans la base fédérale", systemImage: "magnifyingglass") + } + } footer: { + if club.code != nil { + HStack { + Spacer() + Button("annuler", role: .cancel) { + updateClubData = false + } + } + } else { + Text("Vous pouvez chercher un club dans la base fédérale et importer les informations directement.") + } + } + } else if let federalLink = club.federalLink() { + Section { + LabeledContent("Code Club") { + Text(club.code ?? "") + } + LabeledContent("Ville") { + Text(club.city ?? "") + } + Link(destination: federalLink) { + Text("Fiche du club sur tenup") + } + } footer: { + HStack { + Spacer() + Button("modifier", role: .destructive) { + updateClubData = true + } + } + } + } + } + .keyboardType(.alphabet) + .autocorrectionDisabled() + .defaultFocus($focusedField, ._name, priority: .automatic) + .navigationTitle(displayContext == .edition ? club.name : "Nouveau club") + .navigationBarTitleDisplayMode(.inline) + .toolbar(.visible, for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .onDisappear { + if displayContext == .edition { + try? dataStore.clubs.addOrUpdate(instance: club) + } + } + .onAppear { + if displayContext == .addition { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + focusedField = ._name + } + } + } + } +} + +#Preview { + ClubDetailView(club: Club.mock()) +} diff --git a/PadelClub/Views/Club/ClubImportView.swift b/PadelClub/Views/Club/ClubImportView.swift new file mode 100644 index 0000000..293983d --- /dev/null +++ b/PadelClub/Views/Club/ClubImportView.swift @@ -0,0 +1,43 @@ +// +// ClubImportView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 21/03/2024. +// + +import SwiftUI + +struct ClubImportView: View { + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationStack { + ClubSearchView(displayContext: .addition) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Annuler", role: .cancel) { + dismiss() + } + } +// ToolbarItem(placement: .bottomBar) { +// Button("Valider") { +// try? dataStore.clubs.addOrUpdate(instance: club) +// dismiss() +// } +// .buttonStyle(.borderedProminent) +// .disabled(club.isValid == false) +// } + } + + } + } +} + +#Preview { + ClubImportView() +} + +/* + jQuery.extend(Drupal.settings, {"basePath":"\/","pathPrefix":"","setHasJsCookie":0,"ajaxPageState":{"theme":"met","theme_token":"utLEekigHne808an7j1tJnCcET9qmvCzWWw4NgcPkZ8","jquery_version":"2.2","jquery_version_token":"scWlNljFircoQ0sImdjBmQd-ItZCEG4rYp2CfoxyMbw","css":{"modules\/system\/system.base.css":1,"misc\/ui\/jquery.ui.core.css":1,"misc\/ui\/jquery.ui.theme.css":1,"modules\/field\/theme\/field.css":1,"modules\/node\/node.css":1,"sites\/all\/modules\/contrib\/views\/css\/views.css":1,"sites\/all\/modules\/contrib\/back_to_top\/css\/back_to_top.css":1,"sites\/all\/modules\/contrib\/media\/modules\/media_wysiwyg\/css\/media_wysiwyg.base.css":1,"sites\/all\/libraries\/dynatable\/jquery.dynatable.css":1,"sites\/all\/modules\/custom\/actency_itineraire\/css\/actency_itineraire.css":1,"sites\/all\/modules\/contrib\/ctools\/css\/ctools.css":1,"sites\/all\/modules\/contrib\/video\/css\/video.css":1,"sites\/all\/modules\/contrib\/ctools\/css\/modal.css":1,"https:\/\/cdn.jsdelivr.net\/npm\/bootstrap@3.4.1\/dist\/css\/bootstrap.min.css":1,"https:\/\/cdn.jsdelivr.net\/npm\/@unicorn-fail\/drupal-bootstrap-styles@0.0.2\/dist\/3.3.1\/7.x-3.x\/drupal-bootstrap.min.css":1,"https:\/\/stackpath.bootstrapcdn.com\/font-awesome\/4.7.0\/css\/font-awesome.min.css":1,"sites\/all\/modules\/custom\/actency_background\/css\/actency_background.css":1,"sites\/all\/themes\/met\/public\/css\/met.css":1},"js":{"https:\/\/cdn.jsdelivr.net\/npm\/bootstrap@3.4.1\/dist\/js\/bootstrap.min.js":1,"sites\/all\/themes\/met\/public\/js\/club.js":1,"\/\/cdn.tagcommander.com\/3288\/tc_FFT_19.js":1,"sites\/all\/modules\/custom\/met_tagcommander\/js\/met_tagcommander.js":1,"sites\/all\/themes\/bootstrap\/js\/bootstrap.js":1,"sites\/all\/modules\/contrib\/jquery_update\/replace\/jquery\/2.2\/jquery.min.js":1,"misc\/jquery-extend-3.4.0.js":1,"misc\/jquery-html-prefilter-3.5.0-backport.js":1,"misc\/jquery.once.js":1,"misc\/drupal.js":1,"sites\/all\/modules\/contrib\/jquery_update\/js\/jquery_browser.js":1,"sites\/all\/modules\/contrib\/jquery_update\/replace\/ui\/ui\/minified\/jquery.ui.core.min.js":1,"sites\/all\/modules\/contrib\/jquery_update\/replace\/ui\/ui\/minified\/jquery.ui.effect.min.js":1,"sites\/all\/modules\/contrib\/jquery_update\/replace\/ui\/external\/jquery.cookie.js":1,"sites\/all\/modules\/contrib\/jquery_update\/replace\/jquery.form\/4\/jquery.form.min.js":1,"misc\/ajax.js":1,"sites\/all\/modules\/contrib\/jquery_update\/js\/jquery_update.js":1,"sites\/all\/modules\/custom\/actency_dynatable\/actency_dynatable.js":1,"sites\/all\/modules\/contrib\/entityreference\/js\/entityreference.js":1,"sites\/all\/modules\/contrib\/back_to_top\/js\/back_to_top.js":1,"public:\/\/languages\/fr_WZMeukhvWvW_cblXuy_3SROUpPpfX7YEK6xml2YasT8.js":1,"sites\/all\/libraries\/dynatable\/jquery.dynatable.js":1,"sites\/all\/modules\/custom\/actency_popup_message\/js\/actency_popup_message.js":1,"sites\/all\/modules\/contrib\/image_caption\/image_caption.min.js":1,"sites\/all\/modules\/contrib\/video\/js\/video.js":1,"sites\/all\/themes\/bootstrap\/js\/misc\/_progress.js":1,"sites\/all\/modules\/contrib\/ctools\/js\/modal.js":1,"sites\/all\/modules\/custom\/user_actency\/js\/user_actency.js":1,"sites\/all\/modules\/custom\/adherent\/js\/adherent_modal.js":1,"sites\/all\/modules\/custom\/met_tagcommander\/js\/met_tagcommander_vuejs.js":1,"\/\/cdn.tagcommander.com\/3288\/tc_FFT_18.js":1,"sites\/all\/modules\/custom\/tagcommander_click\/js\/tagcommander_click.js":1,"sites\/all\/modules\/custom\/actency_tournoi_favoris\/js\/actency_tournoi_favoris.js":1,"sites\/all\/themes\/met\/js\/met.js":1,"sites\/all\/themes\/bootstrap\/js\/modules\/ctools\/js\/modal.js":1,"sites\/all\/themes\/bootstrap\/js\/misc\/ajax.js":1}},"fft_club":{"fiche":{"nom":"T.C ROQUEFORT-LA-BEDOULE","labels":[{"class":"galaxie","label":"\u00c9cole de Tennis Galaxie (d\u00e8s 3 ans)"},{"class":"fauteuil","label":"Tennis Fauteuil"}]},"labelsRecompenses":[{"class":["meticon-engage","yellow"],"label":"CLUB ENGAG\u00c9","detail":"xxx"}],"description":"","installations":[{"nom":"T.C.R.B.","bgClass":"tennis","photoPrincipale":"","nbTerrainsSurfacesText":"5 terrains de 3 surfaces","adresse":"110 AV Michelangeli - 13830 ROQUEFORT-LA-BEDOULE","pratiques":[{"cssClass":"meticon-racket-tennis-inclined","label":"TENNIS","typesSurfaces":{"dur":{"cssClass":"meticon-surface-hard","natures":{"bp":{"label":"B\u00e9ton poreux","nbTerrainsSurfacesText":"2 terrains"}}},"gazon":{"cssClass":"meticon-surface-other","natures":{"gas":{"label":"Gazon synth\u00e9tique","nbTerrainsSurfacesText":"2 terrains"}}},"autre":{"cssClass":"meticon-ellipsis-v","natures":{"par":{"label":"Parquet","nbTerrainsSurfacesText":"1 terrain dont 1 couvert"}}}}}],"services":{"vestiaires":{"iconClass":"meticon-clothes","label":"Vestiaires","messageInfos":""},"parking":{"iconClass":"meticon-parking","label":"Parking","messageInfos":""},"wifi":{"iconClass":"meticon-wifi","label":"Wifi","messageInfos":"Non disponible"},"television":{"iconClass":"meticon-tv","label":"TV","messageInfos":"Non disponible"},"pmr":{"cssClass":"details-PMR","iconClass":"meticon-wheelchair","label":"Accessibilit\u00e9 PMR","listeServices":[{"class":"active","label":"DOUCHES"},{"class":"active","label":"SANITAIRES"},{"class":"active","label":"TERRAINS"},{"class":"active","label":"CLUB HOUSE"}]}},"horaires":[],"adresseUrl":"https:\/\/www.google.com\/maps\/dir\/?api=1\u0026destination=43.248363,5.585647","photoPlaceholder":"\/sites\/all\/modules\/custom\/fft_vuejs\/modules\/vuejs_club\/img\/installation_default.png"}],"enseignants":[{"diplomeCode":"BE2","fonctionCode":"DS","imageSrc":"\/sites\/all\/modules\/custom\/actency_user_picture\/images\/identification.svg","libelleDiplome":"Professeur","libelleCompletDiplome":"Brevet d\u0027Etat 2\u00e8me degr\u00e9 - Professeur","nom":"TASSARO","nomComplet":"Herv\u00e9 TASSARO","fonction":"Directeur Sportif","specialites":["Tennis sant\u00e9"]},{"diplomeCode":"DE","fonctionCode":"ETP","imageSrc":"\/sites\/all\/modules\/custom\/actency_user_picture\/images\/identification.svg","libelleDiplome":"Moniteur","libelleCompletDiplome":"Dipl\u00f4me d\u0027Etat JEPS Tennis - Moniteur","nom":"TASSARO","nomComplet":"Bastien TASSARO","fonction":"Enseignant tous publics","specialites":[]},{"diplomeCode":"DE","fonctionCode":"ETP","imageSrc":"\/sites\/all\/modules\/custom\/actency_user_picture\/images\/identification.svg","libelleDiplome":"Moniteur","libelleCompletDiplome":"Dipl\u00f4me d\u0027Etat JEPS Tennis - Moniteur","nom":"TASSARO","nomComplet":"GABRIEL TASSARO","fonction":"Enseignant tous publics","specialites":["Padel"]}],"periodes":[{"titre":"Tennis","libelle":"Jan. - F\u00e9v. - Mar. - Avr. - Mai - Juin - Juil. - Ao\u00fb. - Sep. - Oct. - Nov. - D\u00e9c."}],"equipements":[],"services":[{"iconClass":"active","label":"Service de cordage"},{"iconClass":"active","label":"Pr\u00eat de mat\u00e9riel"},{"iconClass":"active","label":"Vente de mat\u00e9riel"}],"effectifs":[{"millesime":"saison 2023 - 2024","jeune":188,"adulte":132,"femme":89,"homme":231,"total":320},{"millesime":"saison 2022 - 2023","jeune":226,"adulte":144,"femme":106,"homme":264,"total":370},{"millesime":"saison 2021 - 2022","jeune":206,"adulte":134,"femme":87,"homme":253,"total":340}],"navigation":[{"label":"Club","href":"\/club\/62130381","actif":true},{"label":"Offres","href":"\/club\/62130381\/offres","actif":false},{"label":"Comp\u00e9titions","href":"\/club\/62130381\/competitions","actif":false},{"label":"\u00c9v\u00e9nements","href":"\/club\/62130381\/evenements","actif":false}],"onglet_actif":"Club","partenaires":[],"question":{"title":"Une question ?","label":"Contactez le club pour en savoir plus.","liste":[{"class":"meticon-phone","label":"T\u00e9l\u00e9phone","infos":"0609546866"},{"class":"meticon-at","label":"Courriel","infos":"\/club\/62130381\/contact\/formulaire"},{"class":"meticon-web","label":"Site web","infos":"http:\/\/www.tc-roquefort-la-bedoule.fr\/"},{"class":"fa fa-facebook","label":"Facebook","infos":"https:\/\/www.facebook.com\/Tennis-Club-de-Roquefort-La-B\u00e9doule"}]},"facebookUrl":"https:\/\/www.facebook.com\/Tennis-Club-de-Roquefort-La-B\u00e9doule"},"vuejs_context":"clubInfo","urlImage":"https:\/\/tenup.fft.fr\/sites\/default\/files\/actency_backgrounds\/propositions_0001_02_0.png","CToolsModal":{"loadingText":"En cours de chargement...","closeText":"Fermer la fen\u00eatre","closeImage":"\u003Cimg class=\u0022img-responsive\u0022 src=\u0022https:\/\/tenup.fft.fr\/sites\/all\/modules\/contrib\/ctools\/images\/icon-close-window.png\u0022 alt=\u0022Fermer la fen\u00eatre\u0022 title=\u0022Fermer la fen\u00eatre\u0022 \/\u003E","throbber":"\u003Cimg class=\u0022img-responsive\u0022 src=\u0022https:\/\/tenup.fft.fr\/sites\/all\/modules\/contrib\/ctools\/images\/throbber.gif\u0022 alt=\u0022En cours de chargement\u0022 title=\u0022En cours de chargement...\u0022 \/\u003E"},"happy-modal-style":{"modalSize":{"type":"fixed","width":320,"max-width":360,"height":180,"addWidth":10,"addHeight":10,"contentRight":10,"contentLeft":10,"contentBottom":10,"top":0},"modalClass":"user-actency-cancel","modalOptions":{"opacity":0.6,"background-color":"#000"},"styleCss":"user_cancel_class"},"better_exposed_filters":{"views":{"partenaires":{"displays":{"block":{"filters":[]}}}}},"back_to_top":{"back_to_top_button_trigger":"100","back_to_top_button_text":"Back to top","#attached":{"library":[["system","ui"]]}},"bootstrap":{"anchorsFix":"0","anchorsSmoothScrolling":"0","formHasError":1,"popoverEnabled":1,"popoverOptions":{"animation":1,"html":0,"placement":"right","selector":"","trigger":"click","triggerAutoclose":1,"title":"","content":"","delay":0,"container":"body"},"tooltipEnabled":1,"tooltipOptions":{"animation":1,"html":0,"placement":"auto left","selector":"","trigger":"hover focus","delay":0,"container":"body"}}}); + + */ diff --git a/PadelClub/Views/Club/ClubSearchView.swift b/PadelClub/Views/Club/ClubSearchView.swift new file mode 100644 index 0000000..e11974f --- /dev/null +++ b/PadelClub/Views/Club/ClubSearchView.swift @@ -0,0 +1,381 @@ +// +// 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 { + 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(title: "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(title: "Chercher autour de moi") { + if locationManager.manager.authorizationStatus == .notDetermined { + locationManager.manager.requestWhenInUseAuthorization() + } else if locationManager.manager.authorizationStatus == .denied { + showingSettingsAlert = true + } else { + locationManager.requestLocation() + } + } + } + RowButtonView(title: "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() +} diff --git a/PadelClub/Views/Club/ClubsView.swift b/PadelClub/Views/Club/ClubsView.swift new file mode 100644 index 0000000..4bf53ac --- /dev/null +++ b/PadelClub/Views/Club/ClubsView.swift @@ -0,0 +1,94 @@ +// +// ClubsView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 20/03/2024. +// + +import SwiftUI +import TipKit + +struct ClubsView: View { + @EnvironmentObject var dataStore: DataStore + @State private var presentClubCreationView: Bool = false + @State private var presentClubSearchView: Bool = false + let tip = SlideToDeleteTip() + + var body: some View { + List { + ForEach(dataStore.clubs) { club in + NavigationLink { + ClubDetailView(club: club) + } label: { + Text(club.name) + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button(role: .destructive) { + try? dataStore.clubs.delete(instance: club) + } label: { + Label("Effacer", systemImage: "trash") + } + } + } + + Section { + if dataStore.clubs.isEmpty == false { + TipView(tip) + .tipStyle(tint: nil) + } + } + + } + .overlay { + if dataStore.clubs.isEmpty { + ContentUnavailableView { + Label("Aucun club", systemImage: "house.and.flag.fill") + } description: { + Text("Texte décrivant l'utilité d'un club et les features que cela apporte") + } actions: { + RowButtonView(title: "Créer un nouveau club", systemImage: "plus.circle.fill") { + presentClubCreationView = true + } + RowButtonView(title: "Chercher un club", systemImage: "magnifyingglass.circle.fill") { + presentClubSearchView = true + } + } + } + } + .navigationTitle("Mes clubs") + .sheet(isPresented: $presentClubCreationView) { + CreateClubView() + } + .sheet(isPresented: $presentClubSearchView) { + ClubImportView() + } + .toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + Button { + presentClubSearchView = true + } label: { + Image(systemName: "magnifyingglass.circle.fill") + .resizable() + .scaledToFit() + .frame(minHeight: 28) + } + + Button { + presentClubCreationView = true + } label: { + Image(systemName: "plus.circle.fill") + .resizable() + .scaledToFit() + .frame(minHeight: 28) + } + } + } + } +} + +#Preview { + NavigationStack { + ClubsView() + .environmentObject(DataStore.shared) + } +} diff --git a/PadelClub/Views/Club/CreateClubView.swift b/PadelClub/Views/Club/CreateClubView.swift new file mode 100644 index 0000000..7e06db3 --- /dev/null +++ b/PadelClub/Views/Club/CreateClubView.swift @@ -0,0 +1,46 @@ +// +// CreateClubView.swift +// PadelClub +// +// Created by Razmig Sarkissian on 20/03/2024. +// + +import SwiftUI + +struct CreateClubView: View { + @Bindable var club: Club + @EnvironmentObject var dataStore: DataStore + @Environment(\.dismiss) var dismiss + + init() { + self.club = Club.newEmptyInstance() + } + + + var body: some View { + NavigationStack { + ClubDetailView(club: club, displayContext: .addition) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Annuler", role: .cancel) { + dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Valider") { + try? dataStore.clubs.addOrUpdate(instance: club) + dismiss() + } + .clipShape(Capsule()) + .buttonStyle(.bordered) + .disabled(club.isValid == false) + } + } + } + } +} + +#Preview { + CreateClubView() + .environmentObject(DataStore.shared) +} diff --git a/PadelClub/Views/ClubView.swift b/PadelClub/Views/ClubView.swift index 596e946..37eb36f 100644 --- a/PadelClub/Views/ClubView.swift +++ b/PadelClub/Views/ClubView.swift @@ -19,5 +19,5 @@ struct ClubView: View { } #Preview { - ClubView(club: Club(name: "AUC", address: "")) + ClubView(club: Club(name: "AUC", acronym: "test", address: "")) } diff --git a/PadelClub/Views/Components/RowButtonView.swift b/PadelClub/Views/Components/RowButtonView.swift index b8db9aa..4aa12bd 100644 --- a/PadelClub/Views/Components/RowButtonView.swift +++ b/PadelClub/Views/Components/RowButtonView.swift @@ -11,6 +11,7 @@ struct RowButtonView: View { let title: String var systemImage: String? = nil var image: String? = nil + var animatedProgress: Bool = false let action: () -> () var body: some View { @@ -18,23 +19,32 @@ struct RowButtonView: View { action() } label: { HStack { - Spacer() - if let systemImage { - Image(systemName: systemImage) - } - if let image { - Image(image) - .resizable() - .scaledToFit() - .frame(width: 32, height: 32) + if animatedProgress { + Spacer() + ProgressView() + } else { + if let systemImage { + Image(systemName: systemImage) + .resizable() + .scaledToFit() + .frame(height: 24) + } + if let image { + Image(image) + .resizable() + .scaledToFit() + .frame(width: 32, height: 32) + } + Spacer() + Text(title) + .foregroundColor(.white) + .frame(height: 32) } - Text(title) - .foregroundColor(.white) - .frame(height: 32) Spacer() } .font(.headline) } + .disabled(animatedProgress) .frame(maxWidth: .infinity) .buttonStyle(.borderedProminent) .tint(.launchScreenBackground) diff --git a/PadelClub/Views/ContentView.swift b/PadelClub/Views/ContentView.swift index 4019072..050d62f 100644 --- a/PadelClub/Views/ContentView.swift +++ b/PadelClub/Views/ContentView.swift @@ -64,7 +64,7 @@ struct ContentView: View { var clubs: [Club] = [] for _ in 0...20 { let id = (0...1000000).randomElement()! - let club: Club = Club(name: "test\(id)", address: "some address") + let club: Club = Club(name: "test\(id)", acronym: "test", address: "some address") clubs.append(club) } do { diff --git a/PadelClub/Views/Navigation/Umpire/UmpireView.swift b/PadelClub/Views/Navigation/Umpire/UmpireView.swift index 4820bc2..4a9e97a 100644 --- a/PadelClub/Views/Navigation/Umpire/UmpireView.swift +++ b/PadelClub/Views/Navigation/Umpire/UmpireView.swift @@ -22,6 +22,14 @@ struct UmpireView: View { } label: { Label("Abonnement", systemImage: "tennisball.circle.fill") } + + Section { + NavigationLink { + ClubsView() + } label: { + Label("Mes clubs favoris", systemImage: "house.and.flag.circle.fill") + } + } } .navigationTitle("Juge-Arbitre") }