wip around me tab

xcode16
Raz 1 year ago
parent 40fc73e6bf
commit 708e0aa481
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 13
      PadelClub/Data/Federal/FederalTournament.swift
  3. 1
      PadelClub/Data/Federal/FederalTournamentHolder.swift
  4. 4
      PadelClub/Data/Tournament.swift
  5. 60
      PadelClub/Utils/Network/NetworkFederalService.swift
  6. 42
      PadelClub/ViewModel/AgendaDestination.swift
  7. 27
      PadelClub/ViewModel/FederalDataViewModel.swift
  8. 5
      PadelClub/ViewModel/Selectable.swift
  9. 9
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  10. 173
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  11. 4
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  12. 637
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  13. 79
      PadelClub/Views/Shared/TournamentFilterView.swift

@ -247,6 +247,7 @@
FFCFC0182BBC5A6800B82851 /* SetLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */; };
FFCFC01A2BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */; };
FFCFC01C2BBC5AAA00B82851 /* SetDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */; };
FFD655D82C8DE27400E5B35E /* TournamentLookUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */; };
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */; };
FFDB1C6D2BB2A02000F1E467 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */; };
FFDB1C732BB2CFE900F1E467 /* MySortDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFDB1C722BB2CFE900F1E467 /* MySortDescriptor.swift */; };
@ -593,6 +594,7 @@
FFCFC0172BBC5A6800B82851 /* SetLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetLabelView.swift; sourceTree = "<group>"; };
FFCFC0192BBC5A8500B82851 /* MatchTypeSmallSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSmallSelectionView.swift; sourceTree = "<group>"; };
FFCFC01B2BBC5AAA00B82851 /* SetDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDescriptor.swift; sourceTree = "<group>"; };
FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentLookUpView.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>"; };
FFDB1C6C2BB2A02000F1E467 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
@ -1291,6 +1293,7 @@
FF82CFC82B9132AF00B0CAF2 /* ActivityView.swift */,
FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */,
FF5D0D8A2BB4D1E3005CB568 /* CalendarView.swift */,
FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */,
);
path = Agenda;
sourceTree = "<group>";
@ -1557,6 +1560,7 @@
FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */,
FF8F26412BADFC8700650388 /* TournamentInitView.swift in Sources */,
C4A47D8A2B7BBB6500ADC637 /* SubscriptionView.swift in Sources */,
FFD655D82C8DE27400E5B35E /* TournamentLookUpView.swift in Sources */,
FF1DC5572BAB3AED00FD8220 /* ClubsView.swift in Sources */,
FFE103122C366E5900684FC9 /* ImagePickerView.swift in Sources */,
FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */,

@ -126,10 +126,22 @@ struct FederalTournament: Identifiable, Codable {
?? []
}
var federalClub: FederalClub? {
if let codeClub {
return FederalClub(federalClubCode: codeClub, federalClubName: clubLabel())
} else {
return nil
}
}
var shareMessage: String {
[libelle, dateDebut?.formatted(date: .complete, time: .omitted)].compactMap({$0}).joined(separator: "\n") + "\n"
}
var japMessage: String {
[nomClub, jugeArbitre?.nom, jugeArbitre?.prenom, courrielEngagement, installation?.telephone].compactMap({$0}).joined(separator: ";")
}
func validForSearch(_ searchText: String, scope: FederalTournamentSearchScope) -> Bool {
var trimmedSearchText = searchText.lowercased().trimmingCharacters(in: .whitespaces).folding(options: .diacriticInsensitive, locale: .current)
trimmedSearchText = trimmedSearchText.replaceCharactersFromSet(characterSet: .punctuationCharacters, replacementString: " ")
@ -156,7 +168,6 @@ extension FederalTournament: FederalTournamentHolder {
// var importedId: Int { id }
var holderId: String { id.string }
func clubLabel() -> String {
nomClub ?? villeEngagement ?? installation?.nom ?? ""
}

@ -11,6 +11,7 @@ protocol FederalTournamentHolder {
var holderId: String { get }
var startDate: Date { get }
var endDate: Date? { get }
var codeClub: String? { get }
var tournaments: [any TournamentBuildHolder] { get }
func clubLabel() -> String
func subtitleLabel() -> String

@ -2115,6 +2115,10 @@ extension Tournament: Hashable {
}
extension Tournament: FederalTournamentHolder {
var codeClub: String? {
club()?.code
}
var holderId: String { id }
func clubLabel() -> String {

@ -194,4 +194,64 @@ recherche_type=club&club[autocomplete][value_container][value_field]=\(codeClub.
print("no data found in html")
}
}
func getAllFederalTournaments(sortingOption: String, page: Int, startDate: Date, endDate: Date, city: String, distance: Double, categories: [TournamentCategory], levels: [TournamentLevel], lat: String?, lng: String?, ages: [FederalTournamentAge], types: [FederalTournamentType], nationalCup: Bool) async throws -> [HttpCommand] {
var cityParameter = ""
var searchType = "ligue"
if city.trimmed.isEmpty == false {
searchType = "ville"
cityParameter = city
}
var levelsParameter = ""
if levels.isEmpty == false {
levelsParameter = levels.map { "categorie_tournoi[\($0.localizedLabel)]=\($0.localizedLabel)" }.joined(separator: "&") + "&"
}
var categoriesParameter = ""
if categories.isEmpty == false {
categoriesParameter = categories.map { "epreuve[\($0.requestLabel)]=\($0.requestLabel)" }.joined(separator: "&") + "&"
}
var agesParameter = ""
if ages.isEmpty == false {
agesParameter = ages.map { "categorie_age[\($0.rawValue)]=\($0.rawValue)" }.joined(separator: "&") + "&"
}
var typesParameter = ""
if types.isEmpty == false {
typesParameter = types.map { "type[\($0.rawValue.capitalized)]=\($0.rawValue.capitalized)" }.joined(separator: "&") + "&"
}
var npc = ""
if nationalCup {
npc = "&tournoi_npc=1"
}
let parameters = """
recherche_type=\(searchType)&ville%5Bautocomplete%5D%5Bcountry%5D=fr&ville%5Bautocomplete%5D%5Btextfield%5D=&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Bvalue_field%5D=\(cityParameter)&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Blabel_field%5D=\(cityParameter)&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Blat_field%5D=\(lat ?? "")&ville%5Bautocomplete%5D%5Bvalue_container%5D%5Blng_field%5D=\(lng ?? "")&ville%5Bdistance%5D%5Bvalue_field%5D=\(Int(distance))&club%5Bautocomplete%5D%5Btextfield%5D=&club%5Bautocomplete%5D%5Bvalue_container%5D%5Bvalue_field%5D=&club%5Bautocomplete%5D%5Bvalue_container%5D%5Blabel_field%5D=&pratique=PADEL&date%5Bstart%5D=\(startDate.twoDigitsYearFormatted)&date%5Bend%5D=\(endDate.twoDigitsYearFormatted)&\(categoriesParameter)\(levelsParameter)\(agesParameter)\(typesParameter)\(npc)&page=\(page)&sort=\(sortingOption)&form_build_id=\(formId)&form_id=recherche_tournois_form&_triggering_element_name=submit_page&_triggering_element_value=Submit+page
"""
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://tenup.fft.fr/system/ajax")!,timeoutInterval: Double.infinity)
request.addValue("application/json, text/javascript, */*; q=0.01", 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; charset=UTF-8", forHTTPHeaderField: "Content-Type")
request.addValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
request.addValue("https://tenup.fft.fr", forHTTPHeaderField: "Origin")
request.addValue("keep-alive", forHTTPHeaderField: "Connection")
request.addValue("https://tenup.fft.fr/recherche/tournois", forHTTPHeaderField: "Referer")
request.addValue("empty", forHTTPHeaderField: "Sec-Fetch-Dest")
request.addValue("cors", forHTTPHeaderField: "Sec-Fetch-Mode")
request.addValue("same-origin", forHTTPHeaderField: "Sec-Fetch-Site")
request.httpMethod = "POST"
request.httpBody = postData
return try await runTenupTask(request: request)
}
}

@ -18,11 +18,7 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
case activity
case history
case tenup
enum ViewStyle {
case list
case calendar
}
case around
var localizedTitleKey: String {
switch self {
@ -32,6 +28,8 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
return "Terminé"
case .tenup:
return "Tenup"
case .around:
return "Autour"
}
}
@ -39,14 +37,12 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
localizedTitleKey
}
var systemImage: String {
func systemImage() -> String? {
switch self {
case .activity:
return "squares.leading.rectangle"
case .history:
return "book.closed"
case .tenup:
return "tennisball"
case .around:
return "location.magnifyingglass"
default:
return nil
}
}
@ -58,6 +54,9 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
DataStore.shared.tournaments.filter { $0.endDate != nil && FederalDataViewModel.shared.isTournamentValidForFilters($0) }.count
case .tenup:
FederalDataViewModel.shared.filteredFederalTournaments.map { $0.tournaments.count }.reduce(0,+)
case .around:
nil
}
}
@ -84,6 +83,25 @@ enum AgendaDestination: Int, CaseIterable, Identifiable, Selectable, Equatable {
} else {
return nil
}
case .around:
return nil
}
}
}
enum ViewStyle {
case list
case calendar
}
struct ViewStyleKey: EnvironmentKey {
static let defaultValue: ViewStyle = .list
}
extension EnvironmentValues {
var viewStyle: ViewStyle {
get { self[ViewStyleKey.self] }
set { self[ViewStyleKey.self] = newValue }
}
}

@ -13,11 +13,13 @@ class FederalDataViewModel {
static let shared = FederalDataViewModel()
var federalTournaments: [FederalTournament] = []
var searchedFederalTournaments: [FederalTournament] = []
var levels: Set<TournamentLevel> = Set()
var categories: Set<TournamentCategory> = Set()
var ageCategories: Set<FederalTournamentAge> = Set()
var selectedClubs: Set<String> = Set()
var id: UUID = UUID()
var searchAttemptCount: Int = 0
func filterStatus() -> String {
var labels: [String] = []
@ -33,6 +35,14 @@ class FederalDataViewModel {
return labels.joined(separator: ", ")
}
var searchedClubs: [FederalClub] {
searchedFederalTournaments.compactMap { ft in
ft.federalClub
}.uniqued { fc in
fc.federalClubCode
}.sorted(by: \.federalClubName)
}
func selectedClub() -> Club? {
if selectedClubs.isEmpty == false {
return DataStore.shared.clubs.first(where: { $0.code == selectedClubs.first! })
@ -53,8 +63,16 @@ class FederalDataViewModel {
(levels.isEmpty && categories.isEmpty && ageCategories.isEmpty && selectedClubs.isEmpty) == false
}
var filteredFederalTournaments: [FederalTournament] {
federalTournaments.filter({ tournament in
var filteredFederalTournaments: [FederalTournamentHolder] {
filteredFederalTournaments(from: federalTournaments)
}
var filteredSearchedFederalTournaments: [FederalTournamentHolder] {
filteredFederalTournaments(from: searchedFederalTournaments)
}
func filteredFederalTournaments(from tournaments: [any FederalTournamentHolder]) -> [FederalTournamentHolder] {
tournaments.filter({ tournament in
(levels.isEmpty || tournament.tournaments.anySatisfy({ levels.contains($0.level) }))
&&
(categories.isEmpty || tournament.tournaments.anySatisfy({ categories.contains($0.category) }))
@ -103,3 +121,8 @@ class FederalDataViewModel {
}
}
struct FederalClub: Identifiable {
var id: String { federalClubCode }
var federalClubCode: String
var federalClubName: String
}

@ -14,9 +14,14 @@ protocol Selectable {
func badgeImage() -> Badge?
func badgeValueColor() -> Color?
func displayImageIfValueZero() -> Bool
func systemImage() -> String?
}
extension Selectable {
func systemImage() -> String? {
return nil
}
func displayImageIfValueZero() -> Bool {
return false
}

@ -38,8 +38,13 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
Button {
selectedDestination = destination
} label: {
Text(destination.selectionLabel(index: index))
.foregroundStyle(selectedDestination?.id == destination.id ? .white : .black)
if let systemImage = destination.systemImage() {
Image(systemName: systemImage)
.foregroundStyle(selectedDestination?.id == destination.id ? .white : .black)
} else {
Text(destination.selectionLabel(index: index))
.foregroundStyle(selectedDestination?.id == destination.id ? .white : .black)
}
}
.padding()
.background {

@ -16,12 +16,13 @@ struct ActivityView: View {
@State private var presentFilterView: Bool = false
@State private var presentToolbar: Bool = false
@State private var newTournament: Tournament?
@State private var viewStyle: AgendaDestination.ViewStyle = .list
@State private var viewStyle: ViewStyle = .list
@State private var isGatheringFederalTournaments: Bool = false
@State private var error: Error?
@State private var uuid: UUID = UUID()
@State private var presentClubSearchView: Bool = false
@State private var quickAccessScreen: QuickAccessScreen? = nil
@State private var displaySearchView: Bool = false
enum QuickAccessScreen : Identifiable, Hashable {
case inscription(pasteString: String)
@ -67,6 +68,8 @@ struct ActivityView: View {
return endedTournaments
case .tenup:
return federalDataViewModel.filteredFederalTournaments
case .around:
return federalDataViewModel.filteredSearchedFederalTournaments
}
}
@ -81,23 +84,39 @@ struct ActivityView: View {
.buttonBorderShape(.capsule)
}
@ViewBuilder
func _listView() -> some View {
switch navigation.agendaDestination! {
case .activity:
List {
EventListView(tournaments: runningTournaments, sortAscending: true)
}
case .history:
List {
EventListView(tournaments: endedTournaments, sortAscending: false)
}
case .tenup:
List {
EventListView(tournaments: federalDataViewModel.federalTournaments, sortAscending: true)
.id(uuid)
}
case .around:
List {
EventListView(tournaments: federalDataViewModel.searchedFederalTournaments, sortAscending: true)
.id(uuid)
}
}
}
var body: some View {
@Bindable var navigation = navigation
NavigationStack(path: $navigation.path) {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $navigation.agendaDestination, destinations: AgendaDestination.allCases, nilDestinationIsValid: false)
List {
switch navigation.agendaDestination! {
case .activity:
EventListView(tournaments: runningTournaments, viewStyle: viewStyle, sortAscending: true)
case .history:
EventListView(tournaments: endedTournaments, viewStyle: viewStyle, sortAscending: false)
case .tenup:
EventListView(tournaments: federalDataViewModel.federalTournaments, viewStyle: viewStyle, sortAscending: true)
.id(uuid)
}
}
_listView()
.environment(\.viewStyle, viewStyle)
.environment(federalDataViewModel)
.overlay {
if let error, navigation.agendaDestination == .tenup {
@ -119,11 +138,11 @@ struct ActivityView: View {
ContentUnavailableView.search(text: searchText)
} else if federalDataViewModel.areFiltersEnabled() {
ContentUnavailableView {
Text("Aucun résultat")
Text("Aucun tournoi")
} description: {
Text(federalDataViewModel.filterStatus())
Text("Aucun tournoi ne correspond aux fitres que vous avez choisis : \(federalDataViewModel.filterStatus())")
} actions: {
RowButtonView("supprimer le filtre") {
RowButtonView("modifier vos filtres") {
federalDataViewModel.removeFilters()
}
.padding(.horizontal)
@ -137,6 +156,12 @@ struct ActivityView: View {
//.searchable(text: $searchText)
.onAppear { presentToolbar = true }
.onDisappear { presentToolbar = false }
.sheet(isPresented: $displaySearchView) {
NavigationStack {
TournamentLookUpView()
.environment(federalDataViewModel)
}
}
.sheet(item: $newTournament) { tournament in
EventCreationView(tournaments: [tournament], selectedClub: federalDataViewModel.selectedClub())
.environment(navigation)
@ -164,52 +189,65 @@ struct ActivityView: View {
}
}
.toolbar {
if presentToolbar {
//let _activityStatus = _activityStatus()
if federalDataViewModel.areFiltersEnabled() {
ToolbarItem(placement: .status) {
Text(federalDataViewModel.filterStatus())
ToolbarItemGroup(placement: .topBarLeading) {
Button {
switch viewStyle {
case .list:
viewStyle = .calendar
case .calendar:
viewStyle = .list
}
} label: {
Image(systemName: "calendar.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
}
.symbolVariant(viewStyle == .calendar ? .fill : .none)
ToolbarItemGroup(placement: .topBarLeading) {
Button {
switch viewStyle {
case .list:
viewStyle = .calendar
case .calendar:
viewStyle = .list
}
} label: {
Image(systemName: "calendar.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
}
.symbolVariant(viewStyle == .calendar ? .fill : .none)
Button {
presentFilterView.toggle()
} label: {
Image(systemName: "line.3.horizontal.decrease.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
}
.symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none)
Button {
presentFilterView.toggle()
} label: {
Image(systemName: "line.3.horizontal.decrease.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
}
.symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none)
_pasteView()
}
ToolbarItem(placement: .topBarTrailing) {
Button {
newTournament = Tournament.newEmptyInstance()
_pasteView()
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
}
}
if presentToolbar {
if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false {
let filteredSearchedFederalTournaments = federalDataViewModel.filteredSearchedFederalTournaments
ToolbarItem(placement: .topBarTrailing) {
Button {
newTournament = Tournament.newEmptyInstance()
let status : String = filteredSearchedFederalTournaments.count.formatted() + " tournoi" + filteredSearchedFederalTournaments.count.pluralSuffix
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
ToolbarItem(placement: .bottomBar) {
VStack {
Text(status)
FooterButtonView("modifier les critères de recherche") {
displaySearchView = true
}
}
.font(.footnote)
}
} else if federalDataViewModel.areFiltersEnabled() {
ToolbarItem(placement: .status) {
Text(federalDataViewModel.filterStatus())
}
}
}
@ -298,6 +336,8 @@ struct ActivityView: View {
_endedEmptyView()
case .tenup:
_tenupEmptyView()
case .around:
_searchTenupEmptyView()
}
}
@ -363,6 +403,33 @@ struct ActivityView: View {
}
}
@ViewBuilder
private func _searchTenupEmptyView() -> some View {
if federalDataViewModel.searchAttemptCount == 0 {
ContentUnavailableView {
Label("Recherche de tournoi", systemImage: "magnifyingglass")
} description: {
Text("Chercher les tournois autour de vous pour vous aidez à mieux selectionner ce que vous pouvez proposer.")
} actions: {
RowButtonView("Lancer la recherche") {
displaySearchView = true
}
.padding()
}
} else {
ContentUnavailableView {
Label("Aucun tournoi", systemImage: "shield.slash")
} description: {
Text("Aucun tournoi ne correspond aux critères sélectionnés.")
} actions: {
RowButtonView("Modifier vos critères de recherche") {
displaySearchView = true
}
.padding()
}
}
}
}
//#Preview {

@ -12,13 +12,13 @@ struct EventListView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) var navigation: NavigationViewModel
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel
@Environment(\.viewStyle) var viewStyle
let tournaments: [FederalTournamentHolder]
let viewStyle: AgendaDestination.ViewStyle
let sortAscending: Bool
var body: some View {
let groupedTournamentsByDate = Dictionary(grouping: navigation.agendaDestination == .tenup ? federalDataViewModel.filteredFederalTournaments : tournaments) { $0.startDate.startOfMonth }
let groupedTournamentsByDate = Dictionary(grouping: federalDataViewModel.filteredFederalTournaments(from: tournaments)) { $0.startDate.startOfMonth }
switch viewStyle {
case .list:
ForEach(groupedTournamentsByDate.keys.sorted(by: sortAscending ? { $0 < $1 } : { $0 > $1 }), id: \.self) { section in

@ -0,0 +1,637 @@
//
// TournamentLookUpView.swift
// PadelClub
//
// Created by razmig on 08/09/2024.
//
import SwiftUI
import CoreLocation
import CoreLocationUI
struct TournamentLookUpView: View {
@Environment(FederalDataViewModel.self) var federalDataViewModel: FederalDataViewModel
@StateObject var locationManager = LocationManager()
@Environment(\.dismiss) private var dismiss
@State private var searchField: String = ""
@State private var sectionedTournaments: [String: [FederalTournament]] = [:]
@State private var dayPeriod: DayPeriod = .all
@State private var duration: Int = 3
@State var page: Int = 0
@State var total: Int = 0
@State private var tournamentCategories = Set<TournamentCategory.ID>()
@State private var tournamentLevels = Set<TournamentLevel.ID>()
@State private var tournamentAges = Set<FederalTournamentAge.ID>()
@State private var tournamentTypes = Set<FederalTournamentType.ID>()
@State private var searching: Bool = false
@State private var startDate: Date = Date()
@State private var endDate: Date = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
@AppStorage("lastCity") private var city: String = ""
@State private var ligue: String = ""
@AppStorage("lastDistance") private var distance: Double = 30
@AppStorage("lastSortingOption") private var sortingOption: String = "_DIST_"
@State private var requestedToGetAllPages: Bool = false
@AppStorage("lastNationalCup") private var nationalCup: Bool = false
@State private var revealSearchParameters: Bool = true
@State private var searchScope = FederalTournamentSearchScope.all
var tournaments: [FederalTournament] {
federalDataViewModel.searchedFederalTournaments
}
func canShowTournament(_ tournament: FederalTournament) -> Bool {
guard tournament.dayDuration <= duration else { return false }
guard (tournament.dayPeriod == dayPeriod && dayPeriod != .all) || dayPeriod == .all else { return false }
if searchField.isEmpty {
return true
} else {
return tournament.validForSearch(searchField, scope: searchScope)
}
}
var body: some View {
List {
searchParametersView
if tournaments.isEmpty == false && tournaments.count < total && total >= 200 && requestedToGetAllPages == false {
Section {
Text("Il y a beacoup de tournois pour cette requête, êtes-vous sûr de vouloir tout récupérer ? Sinon essayez d'affiner votre recherche.")
Button {
requestedToGetAllPages = true
page += 1
searching = true
Task {
await getNewPage()
searching = false
buildSectionedData()
}
} label: {
Label("Tout voir", systemImage: "arrow.down.circle")
}
}
}
}
.toolbarBackground(.visible, for: .bottomBar, .navigationBar)
.navigationTitle("Chercher un tournoi")
.navigationBarTitleDisplayMode(.inline)
.onChange(of: locationManager.city, perform: { newValue in
if let newValue, city.isEmpty {
city = newValue
}
})
.toolbarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .bottomBar) {
if revealSearchParameters {
FooterButtonView("Lancer la recherche") {
runSearch()
}
.disabled(searching)
} else if searchField.isEmpty == false && searchScope != .all {
let count = _totalVisibleEpreuves().count
VStack {
Text(searchField)
.foregroundStyle(.secondary)
Text(count.formatted() + " tournoi" + count.pluralSuffix)
}
.font(.caption)
} else if searching {
HStack(spacing: 20) {
Spacer()
ProgressView()
if total > 0 {
let percent = Double(tournaments.count) / Double(total)
Text(percent.formatted(.percent.precision(.significantDigits(1...3))) + " en récupération de Tenup")
.font(.caption)
}
Spacer()
}
} else {
let count = _totalVisibleEpreuves().count
Text(count.formatted() + " tournoi" + count.pluralSuffix)
.font(.caption)
}
}
ToolbarItem(placement: .topBarTrailing) {
Menu {
if tournaments.isEmpty == false {
Section {
let preview = SharePreview(Text("Ma recherche de tournois"), icon: Image("PadelClub_logo_fondclair_transparent"))
ShareLink(item: renderedImage ?? Image(systemName: "photo"), preview: preview) {
if renderedImage == nil {
ProgressView()
} else {
Label("Par image (20max)", systemImage: "square.and.arrow.up")
.labelStyle(.titleAndIcon)
}
}
ShareLink(item: pastedTournaments) {
Label("Par texte", systemImage: "square.and.arrow.up")
.labelStyle(.titleAndIcon)
}
ShareLink(item: japList) {
Label("JAP liste", systemImage: "square.and.arrow.up")
.labelStyle(.titleAndIcon)
}
} header: {
Text("Partager les résultats")
}
}
Divider()
Button {
tournamentLevels = Set()
tournamentCategories = Set()
city = ""
locationManager.location = nil
locationManager.city = nil
distance = 30
startDate = Date()
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
sortingOption = "_DIST_"
revealSearchParameters = true
} label: {
Text("Ré-initialiser la recherche")
}
} label: {
Label("Options", systemImage: "ellipsis.circle")
}
}
}
}
var pastedTournaments: String {
tournaments.map { $0.shareMessage }.joined()
}
var japList: String {
Set(tournaments.map { $0.japMessage }).joined(separator: "\n")
}
private func isTypeLookedAfter(_ type: any TournamentBuildHolder) -> Bool {
if levels.contains(where: { level in
type.level == level
}) || levels.isEmpty {
if categories.contains(where: { category in
type.category == category
}) || categories.isEmpty {
return true
}
}
return false
}
@Environment(\.displayScale) var displayScale
@State private var renderedImage: Image?
@MainActor
func render() {
let renderer = ImageRenderer(content: tournamentsView)
renderer.scale = displayScale
renderer.isOpaque = true
if let uiImage = renderer.uiImage {
renderedImage = Image(uiImage: uiImage)
}
}
@ViewBuilder
private var tournamentsView: some View {
let tournaments = tournaments.prefix(20)
VStack {
ForEach(tournaments.indices, id: \.self) { tournamentIndex in
let tournament = tournaments[tournamentIndex]
HStack(alignment: .center) {
VStack(alignment: .leading) {
Text(tournament.libelle ?? "unknown").font(.headline)
if let club = tournament.nomClub {
Text(club)
.font(.footnote)
.lineLimit(1)
}
}
Spacer()
VStack(alignment: .trailing) {
if let startDate = tournament.dateDebut {
Text(startDate.monthYearFormatted)
HStack {
Text(startDate.formatted(.dateTime.weekday()))
Text(startDate.formatted(.dateTime.day())).font(.largeTitle)
}
}
if let distance = tournament.distanceEnMetres {
let measurement = Measurement(value: distance / 1000, unit: UnitLength.kilometers)
Text(measurement.formatted()).font(.caption)
}
}
}
.padding()
.foregroundColor(Color.black)
.background {
tournamentIndex%2 == 0 ? Color.mint : Color.cyan
}
}
}
.padding()
}
private var clubsFound: [String] {
Set(tournaments.compactMap { $0.nomClub }).sorted()
}
private var liguesFound: [String] {
Set(tournaments.compactMap { $0.nomLigue }).sorted()
}
private func runSearch() {
revealSearchParameters = false
total = 0
page = 0
federalDataViewModel.searchedFederalTournaments = []
searching = true
requestedToGetAllPages = false
renderedImage = nil
federalDataViewModel.searchAttemptCount += 1
Task {
await getNewPage()
searching = false
dismiss()
}
}
func buildSectionedData() {
sectionedTournaments = FederalTournament.sectionedData(from: tournaments)
}
private var distanceLimit: Measurement<UnitLength> {
distanceLimit(distance: distance)
}
private func distanceLimit(distance: Double) -> Measurement<UnitLength> {
Measurement(value: distance, unit: .kilometers)
}
private var categories: [TournamentCategory] {
tournamentCategories.compactMap { TournamentCategory(rawValue: $0) }
}
private var levels: [TournamentLevel] {
tournamentLevels.compactMap { TournamentLevel(rawValue: $0) }
}
private var ages: [FederalTournamentAge] {
tournamentAges.compactMap { FederalTournamentAge(rawValue: $0) }
}
private var types: [FederalTournamentType] {
tournamentTypes.compactMap { FederalTournamentType(rawValue: $0) }
}
func getNewPage() async {
do {
if NetworkFederalService.shared.formId.isEmpty {
await getNewBuildForm()
} else {
let commands = try await NetworkFederalService.shared.getAllFederalTournaments(sortingOption: sortingOption, page: page, startDate: startDate, endDate: endDate, city: city, distance: distance, categories: categories, levels: levels, lat: locationManager.location?.coordinate.latitude.formatted(.number.locale(Locale(identifier: "us"))), lng: locationManager.location?.coordinate.longitude.formatted(.number.locale(Locale(identifier: "us"))), ages: ages, types: types, nationalCup: nationalCup)
let resultCommand = commands.first(where: { $0.results != nil })
if let newTournaments = resultCommand?.results?.items {
newTournaments.forEach { ft in
if tournaments.contains(where: { $0.id == ft.id }) == false {
federalDataViewModel.searchedFederalTournaments.append(ft)
}
}
}
if let count = resultCommand?.results?.nb_results {
print("count", count, total, tournaments.count, page)
total = count
if renderedImage == nil {
render()
}
if tournaments.count < count && page < total / 30 {
if total < 200 || requestedToGetAllPages {
page += 1
await MainActor.run() {
buildSectionedData()
}
await getNewPage()
}
} else {
print("finished")
}
} else {
print("total results not found")
}
}
} catch {
print("getNewPage", error)
await getNewBuildForm()
}
}
func getNewBuildForm() async {
do {
try await NetworkFederalService.shared.getNewBuildForm()
await getNewPage()
} catch {
print("getNewBuildForm", error)
}
}
@ViewBuilder
var searchContollerView: some View {
Section {
Button {
runSearch()
} label: {
HStack {
Label("Chercher un tournoi", systemImage: "magnifyingglass")
if searching {
Spacer()
ProgressView()
}
}
}
Button {
tournamentLevels = Set()
tournamentCategories = Set()
city = ""
locationManager.location = nil
locationManager.city = nil
dayPeriod = .all
duration = 3
distance = 30
startDate = Date()
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
sortingOption = "_DIST_"
revealSearchParameters = true
} label: {
Label("Ré-initialiser la recherche", systemImage: "xmark.circle")
}
}
}
@ViewBuilder
var searchParametersView: some View {
Section {
DatePicker("Début", selection: $startDate, displayedComponents: .date)
DatePicker("Fin", selection: $endDate, displayedComponents: .date)
Picker(selection: $duration) {
Text("Aucune").tag(7)
Text(1.formatted()).tag(1)
Text(2.formatted()).tag(2)
Text(3.formatted()).tag(3)
} label: {
Text("Durée max (en jours)")
}
Picker(selection: $dayPeriod) {
Text("N'importe").tag(DayPeriod.all)
Text("le weekend").tag(DayPeriod.weekend)
Text("la semaine").tag(DayPeriod.week)
} label: {
Text("En semaine ou week-end")
}
HStack {
TextField("Ville", text: $city)
if let city = locationManager.city {
Divider()
Text(city).italic()
}
if locationManager.requestStarted {
ProgressView()
} else {
LocationButton {
locationManager.requestLocation()
}
.symbolVariant(.fill)
.foregroundColor (Color.white)
.cornerRadius (20)
.font(.system(size: 12))
}
}
Picker(selection: $distance) {
Text(distanceLimit(distance:30).formatted()).tag(30.0)
Text(distanceLimit(distance:50).formatted()).tag(50.0)
Text(distanceLimit(distance:60).formatted()).tag(60.0)
Text(distanceLimit(distance:90).formatted()).tag(90.0)
Text(distanceLimit(distance:150).formatted()).tag(150.0)
Text(distanceLimit(distance:200).formatted()).tag(200.0)
Text(distanceLimit(distance:400).formatted()).tag(400.0)
Text("Aucune").tag(3000.0)
} label: {
Text("Distance max")
}
Picker(selection: $sortingOption) {
Text("Distance").tag("_DIST_")
Text("Date de début").tag("dateDebut+asc")
Text("Date de fin").tag("dateFin+asc")
} label: {
Text("Trier par")
}
NavigationLink {
List(TournamentCategory.allCases, selection: $tournamentCategories) { type in
Text(type.localizedLabel())
}
.navigationTitle("Catégories")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Catégorie")
Spacer()
categoriesLabel
.foregroundStyle(.secondary)
}
}
NavigationLink {
List(TournamentLevel.allCases, selection: $tournamentLevels) { type in
Text(type.localizedLabel())
}
.navigationTitle("Niveaux")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Niveau")
Spacer()
levelsLabel
.foregroundStyle(.secondary)
}
}
NavigationLink {
List(FederalTournamentAge.allCases, selection: $tournamentAges) { type in
Text(type.localizedLabel())
}
.navigationTitle("Limites d'âge")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Limite d'âge")
Spacer()
if tournamentAges.isEmpty || tournamentAges.count == FederalTournamentAge.allCases.count {
Text("Tous les âges")
.foregroundStyle(.secondary)
} else {
Text(ages.map({ $0.localizedLabel()}).joined(separator: ", "))
.foregroundStyle(.secondary)
}
}
}
NavigationLink {
List(FederalTournamentType.allCases, selection: $tournamentTypes) { type in
Text(type.localizedLabel())
}
.navigationTitle("Types de tournoi")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Type de tournoi")
Spacer()
if tournamentTypes.isEmpty || tournamentTypes.count == FederalTournamentType.allCases.count {
Text("Tous les types")
.foregroundStyle(.secondary)
} else {
Text(types.map({ $0.localizedLabel()}).joined(separator: ", "))
.foregroundStyle(.secondary)
}
}
}
Picker(selection: $nationalCup) {
Text("N'importe").tag(false)
Text("Uniquement").tag(true)
} label: {
Text("Circuit National Padel Cup")
}
} header: {
Text("Critères de recherche")
}
.headerProminence(.increased)
.disabled(searching)
}
var categoriesLabel: some View {
if tournamentCategories.isEmpty || tournamentCategories.count == TournamentCategory.allCases.count {
Text("Toutes les catégories")
} else {
Text(categories.map({ $0.localizedLabel() }).joined(separator: ", "))
}
}
var levelsLabel: some View {
if tournamentLevels.isEmpty || tournamentLevels.count == TournamentLevel.allCases.count {
Text("Tous les niveaux")
} else {
Text(levels.map({ $0.localizedLabel() }).joined(separator: ", "))
}
}
@ViewBuilder
var searchParametersSummaryView: some View {
VStack(alignment: .leading) {
HStack {
Text("Lieu")
Spacer()
Text(city)
if distance >= 3000 {
Text("sans limite de distance")
} else {
Text("à moins de " + distanceLimit.formatted())
}
}
HStack {
Text("Période")
Spacer()
Text("Du")
Text(startDate.twoDigitsYearFormatted)
Text("Au")
Text(endDate.twoDigitsYearFormatted)
}
HStack {
Text("Niveau")
Spacer()
levelsLabel
}
HStack {
Text("Catégorie")
Spacer()
categoriesLabel
}
HStack {
Text("Tri")
Spacer()
Text(sortingOptionLabel)
}
}
}
var sortingOptionLabel: String {
switch sortingOption {
case "_DIST_": return "Distance"
case "dateDebut+asc": return "Date de début"
case "dateFin+asc": return "Date de fin"
default:
return "Distance"
}
}
func _totalVisibleTournaments(_ date: Date? = nil) -> [FederalTournament] {
if let date {
if let tournaments = sectionedTournaments[URL.importDateFormatter.string(from: date)] {
let allTournaments = tournaments.filter({ canShowTournament($0) }).filter({ tournament in
if tournament.tournaments.count > 1 {
return tournament.tournaments.anySatisfy { isTypeLookedAfter($0) }
} else {
return true
}
})
return allTournaments
} else {
return []
}
} else {
let allTournaments = sectionedTournaments.values.flatMap({ $0 }).filter({ canShowTournament($0) }).filter({ tournament in
if tournament.tournaments.count > 1 {
return tournament.tournaments.anySatisfy { isTypeLookedAfter($0) }
} else {
return true
}
})
return allTournaments
}
}
func _totalVisibleEpreuves(_ date: Date? = nil) -> [any TournamentBuildHolder] {
if let date {
if let tournaments = sectionedTournaments[URL.importDateFormatter.string(from: date)] {
let allTournaments = tournaments
.filter({ canShowTournament($0) })
.compactMap({ $0.tournaments })
.flatMap({ $0 })
.filter({ isTypeLookedAfter($0) })
return allTournaments
} else {
return []
}
} else {
let allTournaments = sectionedTournaments.values.flatMap({ $0 })
.filter({ canShowTournament($0) })
.compactMap({ $0.tournaments })
.flatMap({ $0 })
.filter({ isTypeLookedAfter($0) })
return allTournaments
}
}
}

@ -9,12 +9,13 @@ import SwiftUI
struct TournamentFilterView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(NavigationViewModel.self) private var navigation
@Environment(\.dismiss) private var dismiss
@State private var levels: Set<TournamentLevel>
@State private var categories: Set<TournamentCategory>
@State private var ageCategories: Set<FederalTournamentAge>
@State private var selectedClubs: Set<String>
var federalDataViewModel: FederalDataViewModel
@State private var federalDataViewModel: FederalDataViewModel
init(federalDataViewModel: FederalDataViewModel) {
self.federalDataViewModel = federalDataViewModel
@ -27,30 +28,6 @@ struct TournamentFilterView: View {
var body: some View {
NavigationView {
Form {
let clubs : [Club] = dataStore.user.clubsObjects()
if clubs.filter({ $0.code != nil }).isEmpty == false {
Section {
ForEach(clubs.filter({ $0.code != nil })) { club in
LabeledContent {
Button {
if selectedClubs.contains(club.code!) {
selectedClubs.remove(club.code!)
} else {
selectedClubs.insert(club.code!)
}
} label: {
if selectedClubs.contains(club.code!) {
Image(systemName: "checkmark.circle.fill")
}
}
} label: {
Text(club.clubTitle())
}
}
} header: {
Text("Clubs")
}
}
Section {
ForEach(TournamentLevel.allCases) { level in
LabeledContent {
@ -116,6 +93,58 @@ struct TournamentFilterView: View {
} header: {
Text("Catégories d'âge")
}
if navigation.agendaDestination == .around {
let clubs : [FederalClub] = federalDataViewModel.searchedClubs
if clubs.isEmpty == false {
Section {
ForEach(clubs) { club in
LabeledContent {
Button {
if selectedClubs.contains(club.federalClubCode) {
selectedClubs.remove(club.federalClubCode)
} else {
selectedClubs.insert(club.federalClubCode)
}
} label: {
if selectedClubs.contains(club.federalClubCode) {
Image(systemName: "checkmark.circle.fill")
}
}
} label: {
Text(club.federalClubName)
}
}
} header: {
Text("Clubs")
}
}
} else {
let clubs : [Club] = dataStore.user.clubsObjects().filter({ $0.code != nil })
if clubs.isEmpty == false {
Section {
ForEach(clubs) { club in
LabeledContent {
Button {
if selectedClubs.contains(club.code!) {
selectedClubs.remove(club.code!)
} else {
selectedClubs.insert(club.code!)
}
} label: {
if selectedClubs.contains(club.code!) {
Image(systemName: "checkmark.circle.fill")
}
}
} label: {
Text(club.clubTitle())
}
}
} header: {
Text("Clubs")
}
}
}
}
.toolbar {
ToolbarItem(placement: .topBarLeading) {

Loading…
Cancel
Save