update tournament search feature

xcode16
Raz 1 year ago
parent 708e0aa481
commit 8e301a4f24
  1. 15
      PadelClub/Data/Federal/FederalTournament.swift
  2. 1
      PadelClub/Data/Federal/FederalTournamentHolder.swift
  3. 10
      PadelClub/Data/Tournament.swift
  4. 24
      PadelClub/ViewModel/FederalDataViewModel.swift
  5. 3
      PadelClub/Views/Components/GenericDestinationPickerView.swift
  6. 142
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  7. 226
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  8. 20
      PadelClub/Views/Shared/TournamentFilterView.swift

@ -7,10 +7,23 @@ import Foundation
import CoreLocation
import LeStorage
enum DayPeriod {
enum DayPeriod: CaseIterable, Identifiable {
var id: Self { self }
case all
case weekend
case week
func localizedDayPeriodLabel() -> String {
switch self {
case .all:
return "n'importe"
case .week:
return "la semaine"
case .weekend:
return "le week-end"
}
}
}
// MARK: - FederalTournament

@ -16,6 +16,7 @@ protocol FederalTournamentHolder {
func clubLabel() -> String
func subtitleLabel() -> String
var dayDuration: Int { get }
var dayPeriod: DayPeriod { get }
}
extension FederalTournamentHolder {

@ -2134,6 +2134,16 @@ extension Tournament: FederalTournamentHolder {
self
]
}
var dayPeriod: DayPeriod {
let day = startDate.get(.weekday)
switch day {
case 2...6:
return .week
default:
return .weekend
}
}
}
extension Tournament: TournamentBuildHolder {

@ -20,6 +20,8 @@ class FederalDataViewModel {
var selectedClubs: Set<String> = Set()
var id: UUID = UUID()
var searchAttemptCount: Int = 0
var dayDuration: Int?
var dayPeriod: DayPeriod = .all
func filterStatus() -> String {
var labels: [String] = []
@ -32,6 +34,12 @@ class FederalDataViewModel {
}
labels.append(contentsOf: clubNames)
if dayPeriod != .all {
labels.append(dayPeriod.localizedDayPeriodLabel())
}
if let dayDuration {
labels.append("max " + dayDuration.formatted() + " jour" + dayDuration.pluralSuffix)
}
return labels.joined(separator: ", ")
}
@ -56,11 +64,13 @@ class FederalDataViewModel {
categories.removeAll()
ageCategories.removeAll()
selectedClubs.removeAll()
dayPeriod = .all
dayDuration = nil
id = UUID()
}
func areFiltersEnabled() -> Bool {
(levels.isEmpty && categories.isEmpty && ageCategories.isEmpty && selectedClubs.isEmpty) == false
(levels.isEmpty && categories.isEmpty && ageCategories.isEmpty && selectedClubs.isEmpty && dayPeriod == .all && dayDuration == nil) == false
}
var filteredFederalTournaments: [FederalTournamentHolder] {
@ -80,6 +90,10 @@ class FederalDataViewModel {
(ageCategories.isEmpty || tournament.tournaments.anySatisfy({ ageCategories.contains($0.age) }))
&&
(selectedClubs.isEmpty || selectedClubs.contains(tournament.codeClub!))
&&
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
})
}
@ -90,6 +104,10 @@ class FederalDataViewModel {
(categories.isEmpty || categories.contains(tournament.category))
&&
(ageCategories.isEmpty || ageCategories.contains(tournament.age))
&&
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
if let codeClub = tournament.club()?.code {
return firstPart && (selectedClubs.isEmpty || selectedClubs.contains(codeClub))
@ -106,6 +124,10 @@ class FederalDataViewModel {
(ageCategories.isEmpty || ageCategories.contains(build.age))
&&
(selectedClubs.isEmpty || selectedClubs.contains(tournament.codeClub!))
&&
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
}
func gatherTournaments(clubs: [Club], startDate: Date, endDate: Date? = nil) async throws {

@ -23,6 +23,7 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
} label: {
Image(systemName: "wrench.and.screwdriver")
.foregroundColor(selectedDestination == nil ? .white : .black)
.contentShape(Capsule())
}
.padding()
.background {
@ -41,9 +42,11 @@ struct GenericDestinationPickerView<T: Identifiable & Selectable & Equatable >:
if let systemImage = destination.systemImage() {
Image(systemName: systemImage)
.foregroundStyle(selectedDestination?.id == destination.id ? .white : .black)
.contentShape(Capsule())
} else {
Text(destination.selectionLabel(index: index))
.foregroundStyle(selectedDestination?.id == destination.id ? .white : .black)
.contentShape(Capsule())
}
}
.padding()

@ -84,38 +84,25 @@ struct ActivityView: View {
.buttonBorderShape(.capsule)
}
@ViewBuilder
func _listView() -> some View {
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:
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)
_listView()
.environment(\.viewStyle, viewStyle)
.environment(federalDataViewModel)
.overlay {
@ -142,10 +129,14 @@ struct ActivityView: View {
} description: {
Text("Aucun tournoi ne correspond aux fitres que vous avez choisis : \(federalDataViewModel.filterStatus())")
} actions: {
RowButtonView("modifier vos filtres") {
FooterButtonView("supprimer vos filtres") {
federalDataViewModel.removeFilters()
}
.padding(.horizontal)
FooterButtonView("modifier vos filtres") {
presentFilterView = true
}
.padding(.horizontal)
}
} else {
_dataEmptyView()
@ -153,20 +144,10 @@ 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)
.tint(.master)
}
.refreshable {
if navigation.agendaDestination == .tenup {
federalDataViewModel.federalTournaments.removeAll()
@ -182,12 +163,28 @@ struct ActivityView: View {
}
}
.onChange(of: navigation.agendaDestination) {
if tournaments.isEmpty, viewStyle == .calendar {
viewStyle = .list
}
if navigation.agendaDestination == .tenup
&& dataStore.user.hasTenupClubs() == true
&& federalDataViewModel.federalTournaments.isEmpty {
_gatherFederalTournaments()
}
}
.onChange(of: presentFilterView, { old, new in
if old == true, new == false { //closing filter view
if tournaments.isEmpty, viewStyle == .calendar {
viewStyle = .list
}
}
})
.toolbarTitleDisplayMode(.large)
.navigationTitle(TabDestination.activity.title)
.navigationDestination(for: Tournament.self) { tournament in
TournamentView(tournament: tournament)
}
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
Button {
@ -201,7 +198,7 @@ struct ActivityView: View {
Image(systemName: "calendar.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
.frame(minHeight: 32)
}
.symbolVariant(viewStyle == .calendar ? .fill : .none)
@ -211,7 +208,7 @@ struct ActivityView: View {
Image(systemName: "line.3.horizontal.decrease.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
.frame(minHeight: 32)
}
.symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none)
@ -226,35 +223,42 @@ struct ActivityView: View {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 28)
.frame(minHeight: 32)
}
}
if presentToolbar {
if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false {
let filteredSearchedFederalTournaments = federalDataViewModel.filteredSearchedFederalTournaments
let status : String = filteredSearchedFederalTournaments.count.formatted() + " tournoi" + filteredSearchedFederalTournaments.count.pluralSuffix
if presentToolbar, tournaments.isEmpty == false {
ToolbarItemGroup(placement: .bottomBar) {
VStack(spacing: 0) {
let searchStatus = _searchStatus()
if searchStatus.isEmpty == false {
Text(_searchStatus())
.font(.footnote)
.foregroundStyle(.secondary)
}
ToolbarItem(placement: .bottomBar) {
VStack {
Text(status)
FooterButtonView("modifier les critères de recherche") {
HStack {
if navigation.agendaDestination == .around {
FooterButtonView("modifier votre recherche") {
displaySearchView = true
}
if federalDataViewModel.areFiltersEnabled() {
Text("ou")
}
.font(.footnote)
}
} else if federalDataViewModel.areFiltersEnabled() {
ToolbarItem(placement: .status) {
Text(federalDataViewModel.filterStatus())
if federalDataViewModel.areFiltersEnabled() {
FooterButtonView(_filterButtonTitle()) {
presentFilterView = true
}
}
}
.padding(.bottom, 8)
}
}
}
.navigationTitle(TabDestination.activity.title)
.navigationDestination(for: Tournament.self) { tournament in
TournamentView(tournament: tournament)
}
.sheet(isPresented: $presentFilterView) {
TournamentFilterView(federalDataViewModel: federalDataViewModel)
@ -270,8 +274,17 @@ struct ActivityView: View {
ClubImportView()
.tint(.master)
}
.sheet(isPresented: $displaySearchView) {
NavigationStack {
TournamentLookUpView()
.environment(federalDataViewModel)
}
}
.sheet(item: $newTournament) { tournament in
EventCreationView(tournaments: [tournament], selectedClub: federalDataViewModel.selectedClub())
.environment(navigation)
.tint(.master)
}
.sheet(item: $quickAccessScreen) { screen in
switch screen {
case .inscription(let pasteString):
@ -312,6 +325,31 @@ struct ActivityView: View {
}
}
}
}
private func _searchStatus() -> String {
var searchStatus : [String] = []
if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false {
let filteredSearchedFederalTournaments = federalDataViewModel.filteredSearchedFederalTournaments
let status : String = filteredSearchedFederalTournaments.count.formatted() + " tournoi" + filteredSearchedFederalTournaments.count.pluralSuffix
searchStatus.append(status)
}
if federalDataViewModel.areFiltersEnabled(), tournaments.isEmpty == false {
searchStatus.append(federalDataViewModel.filterStatus())
}
return searchStatus.joined(separator: " ")
}
private func _filterButtonTitle() -> String {
var prefix = "modifier "
if navigation.agendaDestination == .around, federalDataViewModel.searchedFederalTournaments.isEmpty == false {
prefix = ""
}
return prefix + "vos filtres"
}
private func _gatherFederalTournaments() {
isGatheringFederalTournaments = true
@ -422,7 +460,7 @@ struct ActivityView: View {
} description: {
Text("Aucun tournoi ne correspond aux critères sélectionnés.")
} actions: {
RowButtonView("Modifier vos critères de recherche") {
FooterButtonView("modifier vos critères de recherche") {
displaySearchView = true
}
.padding()

@ -15,10 +15,6 @@ struct TournamentLookUpView: View {
@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
@ -32,56 +28,49 @@ struct TournamentLookUpView: View {
@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_"
@AppStorage("lastSortingOption") private var sortingOption: String = "dateDebut+asc"
@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
@State private var presentAlert: Bool = false
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.")
}
.alert("Attention", isPresented: $presentAlert, actions: {
Button {
presentAlert = false
requestedToGetAllPages = true
page += 1
searching = true
Task {
await getNewPage()
searching = false
buildSectionedData()
dismiss()
}
} label: {
Label("Tout voir", systemImage: "arrow.down.circle")
}
Button("Annuler") {
revealSearchParameters = true
presentAlert = false
}
}
}
}, message: {
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.")
})
.toolbarBackground(.visible, for: .bottomBar, .navigationBar)
.navigationTitle("Chercher un tournoi")
.navigationBarTitleDisplayMode(.inline)
.onChange(of: locationManager.city, perform: { newValue in
if let newValue, city.isEmpty {
.onChange(of: locationManager.city) {
if let newValue = locationManager.city, city.isEmpty {
city = newValue
}
})
}
.toolbarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .bottomBar) {
@ -90,14 +79,6 @@ struct TournamentLookUpView: View {
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()
@ -109,25 +90,13 @@ struct TournamentLookUpView: View {
}
Spacer()
}
} else {
let count = _totalVisibleEpreuves().count
Text(count.formatted() + " tournoi" + count.pluralSuffix)
.font(.caption)
}
}
ToolbarItem(placement: .topBarTrailing) {
Menu {
#if DEBUG
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)
@ -141,7 +110,9 @@ struct TournamentLookUpView: View {
}
}
Divider()
Button {
#endif
Button(role: .destructive) {
tournamentLevels = Set()
tournamentCategories = Set()
city = ""
@ -150,8 +121,10 @@ struct TournamentLookUpView: View {
distance = 30
startDate = Date()
endDate = Calendar.current.date(byAdding: .month, value: 3, to: Date())!
sortingOption = "_DIST_"
sortingOption = "dateDebut+asc"
revealSearchParameters = true
federalDataViewModel.searchedFederalTournaments = []
federalDataViewModel.searchAttemptCount = 0
} label: {
Text("Ré-initialiser la recherche")
}
@ -169,75 +142,6 @@ struct TournamentLookUpView: View {
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()
}
@ -253,17 +157,16 @@ struct TournamentLookUpView: View {
federalDataViewModel.searchedFederalTournaments = []
searching = true
requestedToGetAllPages = false
renderedImage = nil
federalDataViewModel.searchAttemptCount += 1
Task {
await getNewPage()
searching = false
if tournaments.isEmpty == false && tournaments.count < total && total >= 200 && requestedToGetAllPages == false {
presentAlert = true
} else {
dismiss()
}
}
func buildSectionedData() {
sectionedTournaments = FederalTournament.sectionedData(from: tournaments)
}
private var distanceLimit: Measurement<UnitLength> {
@ -308,15 +211,9 @@ struct TournamentLookUpView: View {
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 {
@ -361,12 +258,10 @@ struct TournamentLookUpView: View {
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_"
sortingOption = "dateDebut+asc"
revealSearchParameters = true
} label: {
Label("Ré-initialiser la recherche", systemImage: "xmark.circle")
@ -376,23 +271,23 @@ struct TournamentLookUpView: View {
@ViewBuilder
var searchParametersView: some View {
@Bindable var federalDataViewModel = federalDataViewModel
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)
Picker(selection: $federalDataViewModel.dayDuration) {
Text("aucune").tag(nil as Int?)
Text(1.formatted()).tag(1 as Int?)
Text(2.formatted()).tag(2 as Int?)
Text(3.formatted()).tag(3 as Int?)
} 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)
Picker(selection: $federalDataViewModel.dayPeriod) {
ForEach(DayPeriod.allCases) {
Text($0.localizedDayPeriodLabel()).tag($0)
}
} label: {
Text("En semaine ou week-end")
}
@ -585,53 +480,4 @@ struct TournamentLookUpView: View {
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
}
}
}

@ -28,6 +28,26 @@ struct TournamentFilterView: View {
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $federalDataViewModel.dayDuration) {
Text("aucune").tag(nil as Int?)
Text(1.formatted()).tag(1 as Int?)
Text(2.formatted()).tag(2 as Int?)
Text(3.formatted()).tag(3 as Int?)
} label: {
Text("Durée max (en jours)")
}
Picker(selection: $federalDataViewModel.dayPeriod) {
ForEach(DayPeriod.allCases) {
Text($0.localizedDayPeriodLabel()).tag($0)
}
} label: {
Text("En semaine ou week-end")
}
}
Section {
ForEach(TournamentLevel.allCases) { level in
LabeledContent {

Loading…
Cancel
Save