add club management

multistore
Razmig Sarkissian 2 years ago
parent 75d8b1e42b
commit 1ae17aa653
  1. 50
      PadelClub.xcodeproj/project.pbxproj
  2. 72
      PadelClub/Data/Club.swift
  3. 2
      PadelClub/Data/DataStore.swift
  4. 2
      PadelClub/Data/Migration/ClubV1.swift
  5. 16
      PadelClub/Data/MockData.swift
  6. 12
      PadelClub/Extensions/Array+Extensions.swift
  7. 13
      PadelClub/Manager/DisplayContext.swift
  8. 67
      PadelClub/Manager/LocationManager.swift
  9. 81
      PadelClub/Manager/Network/NetworkFederalService.swift
  10. 330
      PadelClub/Manager/Tips.swift
  11. 7
      PadelClub/PadelClubApp.swift
  12. 153
      PadelClub/Views/Club/ClubDetailView.swift
  13. 43
      PadelClub/Views/Club/ClubImportView.swift
  14. 381
      PadelClub/Views/Club/ClubSearchView.swift
  15. 94
      PadelClub/Views/Club/ClubsView.swift
  16. 46
      PadelClub/Views/Club/CreateClubView.swift
  17. 2
      PadelClub/Views/ClubView.swift
  18. 34
      PadelClub/Views/Components/RowButtonView.swift
  19. 2
      PadelClub/Views/ContentView.swift
  20. 8
      PadelClub/Views/Navigation/Umpire/UmpireView.swift

@ -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 = "<group>"; };
C4A47DB02B86375E00ADC637 /* MainUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUserView.swift; sourceTree = "<group>"; };
C4A47DB22B86387500ADC637 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
FF1DC5502BAB351300FD8220 /* ClubDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubDetailView.swift; sourceTree = "<group>"; };
FF1DC5522BAB354A00FD8220 /* MockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockData.swift; sourceTree = "<group>"; };
FF1DC5542BAB36DD00FD8220 /* CreateClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateClubView.swift; sourceTree = "<group>"; };
FF1DC5562BAB3AED00FD8220 /* ClubsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubsView.swift; sourceTree = "<group>"; };
FF1DC5582BAB767000FD8220 /* Tips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tips.swift; sourceTree = "<group>"; };
FF1DC55A2BAB80C400FD8220 /* DisplayContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayContext.swift; sourceTree = "<group>"; };
FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = "<group>"; };
FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
FF3F74F52B919E45004CFE0E /* UmpireView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireView.swift; sourceTree = "<group>"; };
@ -176,6 +192,10 @@
FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InscriptionManagerView.swift; sourceTree = "<group>"; };
FF82CFC42B911F5B00B0CAF2 /* OrganizedTournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizedTournamentView.swift; sourceTree = "<group>"; };
FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = "<group>"; };
FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = "<group>"; };
FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubImportView.swift; sourceTree = "<group>"; };
FFD783FC2B91B9ED000F62A6 /* AgendaDestinationPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaDestinationPickerView.swift; sourceTree = "<group>"; };
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubView.swift; sourceTree = "<group>"; };
FFD784002B91BF79000F62A6 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
@ -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 = "<group>";
};
FF1DC54D2BAB34FA00FD8220 /* Club */ = {
isa = PBXGroup;
children = (
FF1DC5502BAB351300FD8220 /* ClubDetailView.swift */,
FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */,
FF1DC5562BAB3AED00FD8220 /* ClubsView.swift */,
FF1DC5542BAB36DD00FD8220 /* CreateClubView.swift */,
FFC1E10B2BAC7FB0008D6F59 /* ClubImportView.swift */,
);
path = Club;
sourceTree = "<group>";
};
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";

@ -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)")
}
}

@ -45,7 +45,7 @@ class DataStore: ObservableObject {
// store.addMigration(Migration<TournamentV1, TournamentV2>(version: 2))
// store.addMigration(Migration<TournamentV2, Tournament>(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)

@ -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")
}

@ -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: "")
}
}

@ -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)
}
}
}
}

@ -0,0 +1,13 @@
//
// DisplayContext.swift
// PadelClub
//
// Created by Razmig Sarkissian on 20/03/2024.
//
import Foundation
enum DisplayContext {
case addition
case edition
}

@ -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)
}
}

@ -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<T:Decodable>(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)
}
}

@ -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))
}
}

@ -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)
}
}

@ -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())
}

File diff suppressed because one or more lines are too long

@ -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<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(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()
}

@ -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)
}
}

@ -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)
}

@ -19,5 +19,5 @@ struct ClubView: View {
}
#Preview {
ClubView(club: Club(name: "AUC", address: ""))
ClubView(club: Club(name: "AUC", acronym: "test", address: ""))
}

@ -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)

@ -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 {

@ -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")
}

Loading…
Cancel
Save