fix issue with ios 26

add onboarding view
fix bugs
sync3
Razmig Sarkissian 2 months ago
parent 604d24c326
commit 1601f72ad2
  1. 17
      PadelClub.xcodeproj/project.pbxproj
  2. 12
      PadelClub/ViewModel/FederalDataViewModel.swift
  3. 10
      PadelClub/ViewModel/SearchViewModel.swift
  4. 1
      PadelClub/Views/Calling/Components/MenuWarningView.swift
  5. 14
      PadelClub/Views/Components/ButtonValidateView.swift
  6. 12
      PadelClub/Views/Components/Labels.swift
  7. 20
      PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift
  8. 2
      PadelClub/Views/GroupStage/GroupStageView.swift
  9. 8
      PadelClub/Views/GroupStage/GroupStagesSettingsView.swift
  10. 2
      PadelClub/Views/GroupStage/GroupStagesView.swift
  11. 11
      PadelClub/Views/Match/MatchDetailView.swift
  12. 144
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  13. 64
      PadelClub/Views/Navigation/Agenda/TournamentLookUpView.swift
  14. 36
      PadelClub/Views/Navigation/Agenda/WeekdaySelectionView.swift
  15. 17
      PadelClub/Views/Navigation/MainView.swift
  16. 235
      PadelClub/Views/Navigation/OnboardingView.swift
  17. 23
      PadelClub/Views/Score/EditScoreView.swift
  18. 6
      PadelClub/Views/Score/FollowUpMatchView.swift
  19. 97
      PadelClub/Views/Shared/SelectablePlayerListView.swift
  20. 2
      PadelClub/Views/Shared/TournamentFilterView.swift
  21. 2
      PadelClub/Views/Team/TeamRestingView.swift
  22. 10
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift
  23. 2
      PadelClub/Views/Tournament/Screen/PrintSettingsView.swift
  24. 2
      PadelClub/Views/Tournament/Screen/TournamentRankView.swift
  25. 2
      PadelClub/Views/Tournament/TournamentRunningView.swift
  26. 15
      PadelClub/Views/Tournament/TournamentView.swift

@ -711,6 +711,12 @@
FFA252B62CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; };
FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA252B42CDD2C630074E63F /* OngoingDestination.swift */; };
FFA6D7852BB0B795003A31F3 /* FileImportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */; };
FFB0FF672E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; };
FFB0FF682E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; };
FFB0FF692E81B671009EDEAC /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF662E81B671009EDEAC /* OnboardingView.swift */; };
FFB0FF732E841042009EDEAC /* WeekdaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */; };
FFB0FF742E841042009EDEAC /* WeekdaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */; };
FFB0FF752E841042009EDEAC /* WeekdaySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */; };
FFB1C98B2C10255100B154A7 /* TournamentBroadcastRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */; };
FFB378342D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
FFB378352D672ED200EE82E9 /* MatchFormatGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */; };
@ -1112,6 +1118,8 @@
FFA252B42CDD2C630074E63F /* OngoingDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDestination.swift; sourceTree = "<group>"; };
FFA6D7842BB0B795003A31F3 /* FileImportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileImportManager.swift; sourceTree = "<group>"; };
FFA6D78A2BB0BEB3003A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
FFB0FF662E81B671009EDEAC /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekdaySelectionView.swift; sourceTree = "<group>"; };
FFB1C98A2C10255100B154A7 /* TournamentBroadcastRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentBroadcastRowView.swift; sourceTree = "<group>"; };
FFB378332D672ED100EE82E9 /* MatchFormatGuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatGuideView.swift; sourceTree = "<group>"; };
FFBE62042CE9DA0900815D33 /* MatchViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchViewStyle.swift; sourceTree = "<group>"; };
@ -1569,6 +1577,7 @@
isa = PBXGroup;
children = (
FF59FFB62B90EFBF0061EFF9 /* MainView.swift */,
FFB0FF662E81B671009EDEAC /* OnboardingView.swift */,
FFD783FB2B91B919000F62A6 /* Agenda */,
FF3F74FA2B91A04B004CFE0E /* Organizer */,
FF3F74FB2B91A060004CFE0E /* Toolbox */,
@ -1897,6 +1906,7 @@
FF59FFB22B90EFAC0061EFF9 /* EventListView.swift */,
FF5D0D8A2BB4D1E3005CB568 /* CalendarView.swift */,
FFD655D72C8DE27400E5B35E /* TournamentLookUpView.swift */,
FFB0FF722E841042009EDEAC /* WeekdaySelectionView.swift */,
FF8044AB2C8F676D00A49A52 /* TournamentSubscriptionView.swift */,
);
path = Agenda;
@ -2386,7 +2396,9 @@
FF9268072BCE94D90080F940 /* TournamentCallView.swift in Sources */,
FFC2DCB42BBE9ECD0046DB9F /* LoserRoundsView.swift in Sources */,
FF967CFC2BAEE52E00A9A3BD /* GroupStagesView.swift in Sources */,
FFB0FF682E81B671009EDEAC /* OnboardingView.swift in Sources */,
FFD783FF2B91BA42000F62A6 /* PadelClubView.swift in Sources */,
FFB0FF732E841042009EDEAC /* WeekdaySelectionView.swift in Sources */,
C497723A2DC28A92005CD239 /* ComposeViews.swift in Sources */,
FF3A73F32D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FF8F264C2BAE0B4100650388 /* TournamentFormatSelectionView.swift in Sources */,
@ -2651,7 +2663,9 @@
FF4CBFEF2C996C0600151637 /* PadelClubView.swift in Sources */,
FFE8B5CC2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */,
FF3A73F52D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FFB0FF672E81B671009EDEAC /* OnboardingView.swift in Sources */,
C4D05D4A2DC10CBE009B053C /* PaymentStatusView.swift in Sources */,
FFB0FF742E841042009EDEAC /* WeekdaySelectionView.swift in Sources */,
C49772392DC28A92005CD239 /* ComposeViews.swift in Sources */,
FF4CBFF22C996C0600151637 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA592CC02FEB003C7323 /* CoachListView.swift in Sources */,
@ -2894,7 +2908,9 @@
FF70FB6E2C90584900129CC2 /* PadelClubView.swift in Sources */,
FFE8B5CB2DAA42A000BDE966 /* XlsToCsvService.swift in Sources */,
FF3A73F42D37C34D007E3032 /* RegistrationInfoSheetView.swift in Sources */,
FFB0FF692E81B671009EDEAC /* OnboardingView.swift in Sources */,
C4D05D4B2DC10CBE009B053C /* PaymentStatusView.swift in Sources */,
FFB0FF752E841042009EDEAC /* WeekdaySelectionView.swift in Sources */,
C497723B2DC28A92005CD239 /* ComposeViews.swift in Sources */,
FF70FB712C90584900129CC2 /* TournamentFormatSelectionView.swift in Sources */,
FF17CA582CC02FEB003C7323 /* CoachListView.swift in Sources */,
@ -3123,7 +3139,6 @@
DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6;
ENABLE_DEBUG_DYLIB = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PadelClub/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Padel Club";

@ -23,6 +23,7 @@ class FederalDataViewModel {
var searchAttemptCount: Int = 0
var dayDuration: Int?
var dayPeriod: DayPeriod = .all
var weekdays: Set<Int> = Set()
var lastError: NetworkManagerError?
func filterStatus() -> String {
@ -36,6 +37,7 @@ class FederalDataViewModel {
}
labels.append(contentsOf: clubNames.formatList())
labels.append(contentsOf: weekdays.map { Date.weekdays[$0 - 1] }.formatList())
if dayPeriod != .all {
labels.append(dayPeriod.localizedDayPeriodLabel())
}
@ -72,7 +74,7 @@ class FederalDataViewModel {
}
func areFiltersEnabled() -> Bool {
(levels.isEmpty && categories.isEmpty && ageCategories.isEmpty && selectedClubs.isEmpty && dayPeriod == .all && dayDuration == nil) == false
(weekdays.isEmpty && levels.isEmpty && categories.isEmpty && ageCategories.isEmpty && selectedClubs.isEmpty && dayPeriod == .all && dayDuration == nil) == false
}
var filteredFederalTournaments: [FederalTournamentHolder] {
@ -96,6 +98,8 @@ class FederalDataViewModel {
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
&&
(weekdays.isEmpty || weekdays.contains(tournament.startDate.weekDay))
})
}
@ -106,6 +110,8 @@ class FederalDataViewModel {
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
&&
(weekdays.isEmpty || weekdays.contains(tournament.startDate.weekDay))
})
.flatMap { $0.tournaments }
.filter {
@ -137,6 +143,8 @@ class FederalDataViewModel {
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
&&
(weekdays.isEmpty || weekdays.contains(tournament.startDate.weekDay))
if let codeClub = tournament.club()?.code {
return firstPart && (selectedClubs.isEmpty || selectedClubs.contains(codeClub))
@ -157,6 +165,8 @@ class FederalDataViewModel {
(dayPeriod == .all || (dayPeriod != .all && dayPeriod == tournament.dayPeriod))
&&
(dayDuration == nil || (dayDuration != nil && dayDuration == tournament.dayDuration))
&&
(weekdays.isEmpty || weekdays.contains(tournament.startDate.weekDay))
}
func gatherTournaments(clubs: [Club], startDate: Date, endDate: Date? = nil) async throws {

@ -74,6 +74,16 @@ class SearchViewModel: ObservableObject, Identifiable {
}
return message.joined(separator: "\n")
}
func sortTitle() -> String {
var base = [sortOption.localizedLabel()]
base.append((ascending ? "croissant" : "décroissant"))
if selectedAgeCategory != .unlisted {
base.append(selectedAgeCategory.localizedFederalAgeLabel())
}
return base.joined(separator: " ")
}
func codeClubs() -> [String] {
let clubs: [Club] = DataStore.shared.user.clubsObjects()

@ -44,7 +44,6 @@ struct MenuWarningView: View {
}
} label: {
Text("Prévenir")
.underline()
}
.sheet(isPresented: self.$showSubscriptionView, content: {
NavigationStack {

@ -13,10 +13,16 @@ struct ButtonValidateView: View {
let action: () -> ()
var body: some View {
Button(title, role: role) {
action()
if #available(iOS 26.0, *) {
Button(title, systemImage: "checkmark", role: role) {
action()
}
.buttonStyle(.borderedProminent)
} else {
Button(title, role: role) {
action()
}
}
.clipShape(Capsule())
.buttonStyle(.bordered)
}
}

@ -9,7 +9,11 @@ import SwiftUI
struct LabelOptions: View {
var body: some View {
Label("Options", systemImage: "ellipsis.circle")
if #available(iOS 26.0, *) {
Label("Options", systemImage: "ellipsis")
} else {
Label("Options", systemImage: "ellipsis.circle")
}
}
}
@ -39,6 +43,10 @@ struct ShareLabel: View {
struct LabelFilter: View {
var body: some View {
Label("Filtrer", systemImage: "line.3.horizontal.decrease.circle")
if #available(iOS 26.0, *) {
Label("Filtrer", systemImage: "line.3.horizontal.decrease")
} else {
Label("Filtrer", systemImage: "line.3.horizontal.decrease.circle")
}
}
}

@ -126,17 +126,7 @@ struct GroupStageSettingsView: View {
Section {
RowButtonView("Retirer tout le monde", role: .destructive) {
let teams = groupStage.teams()
teams.forEach { team in
team.groupStagePosition = nil
team.groupStage = nil
groupStage._matches().forEach({ $0.updateTeamScores() })
}
do {
try tournamentStore?.teamRegistrations.addOrUpdate(contentOfs: teams)
} catch {
Logger.error(error)
}
groupStage.removeAllTeams()
}
} footer: {
Text("Toutes les équipes seront retirées et les scores des matchs seront perdus.")
@ -188,6 +178,14 @@ struct GroupStageSettingsView: View {
} footer: {
Text("Mets à jour les équipes de la poule si jamais une erreur est persistante.")
}
if tournament.lastStep() == 0 {
RowButtonView("Effacer la poule", role: .destructive) {
tournament.deleteGroupStage(groupStage)
dismiss()
dataStore.tournaments.addOrUpdate(instance: self.tournament)
}
}
}
.onChange(of: size) {
if size != groupStage.size {

@ -70,7 +70,7 @@ struct GroupStageView: View {
}
Section {
MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches), hideWhenEmpty: true)
MatchListView(section: "à lancer", matches: groupStage.readyMatches(playedMatches: playedMatches, runningMatches: runningMatches), hideWhenEmpty: true)
}
Section {

@ -99,6 +99,14 @@ struct GroupStagesSettingsView: View {
}
if tournament.lastStep() == 0, step == 0 {
Section {
RowButtonView("Ajouter une poule", role: .destructive) {
self.tournament.addGroupStage()
dataStore.tournaments.addOrUpdate(instance: self.tournament)
}
}
Section {
RowButtonView("Ajouter une phase de poule", role: .destructive) {
tournament.addNewGroupStageStep()

@ -234,7 +234,7 @@ struct GroupStagesView: View {
Section {
MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches), isExpanded: false)
MatchListView(section: "à lancer", matches: Tournament.readyMatches(allMatches, runningMatches: runningMatches), isExpanded: false)
}
Section {

@ -317,7 +317,18 @@ struct MatchDetailView: View {
})) {
Text(match.confirmed ? "Confirmé" : "Non confirmé")
}
if match.hasWalkoutTeam() == true {
Divider()
Button(role: .destructive) {
match.removeWalkOut()
save()
} label: {
Text("Annuler le forfait")
}
}
Divider()
if match.courtIndex != nil {

@ -25,7 +25,8 @@ struct ActivityView: View {
@State private var quickAccessScreen: QuickAccessScreen? = nil
@State private var displaySearchView: Bool = false
@State private var pasteString: String? = nil
@State private var presentOnboarding: Bool = false
enum QuickAccessScreen : Identifiable, Hashable {
case inscription
@ -77,15 +78,21 @@ struct ActivityView: View {
@ViewBuilder
private func _pasteView() -> some View {
Button {
quickAccessScreen = .inscription
} label: {
Image(systemName: "person.crop.circle.badge.plus")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
if #available(iOS 26.0, *) {
Button("Ajouter une équipe", systemImage: "person.badge.plus") {
quickAccessScreen = .inscription
}
} else {
Button {
quickAccessScreen = .inscription
} label: {
Image(systemName: "person.crop.circle.badge.plus")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
.accessibilityLabel("Ajouter une équipe")
}
.accessibilityLabel("Ajouter une équipe")
// if pasteButtonIsDisplayed == nil || pasteButtonIsDisplayed == true {
// PasteButton(payloadType: String.self) { strings in
@ -222,44 +229,82 @@ struct ActivityView: View {
// print("disappearing", "pasteButtonIsDisplayed", pasteButtonIsDisplayed)
// })
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
Button {
switch viewStyle {
case .list:
viewStyle = .calendar
case .calendar:
viewStyle = .list
ToolbarItem(placement: .topBarLeading) {
if #available(iOS 26.0, *) {
Button("Vue calendrier", systemImage: "calendar") {
switch viewStyle {
case .list:
viewStyle = .calendar
case .calendar:
viewStyle = .list
}
}
} label: {
Image(systemName: "calendar.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
.symbolVariant(viewStyle == .calendar ? .fill : .none)
} else {
Button {
switch viewStyle {
case .list:
viewStyle = .calendar
case .calendar:
viewStyle = .list
}
} label: {
Image(systemName: "calendar.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
.symbolVariant(viewStyle == .calendar ? .fill : .none)
}
.symbolVariant(viewStyle == .calendar ? .fill : .none)
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .topBarLeading)
}
ToolbarItem(placement: .topBarLeading) {
Button {
presentFilterView.toggle()
} label: {
Image(systemName: "line.3.horizontal.decrease.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
if #available(iOS 26.0, *) {
Button("Filtre", systemImage: "line.3.horizontal.decrease") {
presentFilterView.toggle()
}
.symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none)
} else {
Button {
presentFilterView.toggle()
} label: {
Image(systemName: "line.3.horizontal.decrease.circle")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
.symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none)
}
.symbolVariant(federalDataViewModel.areFiltersEnabled() ? .fill : .none)
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .topBarLeading)
}
ToolbarItem(placement: .topBarLeading) {
_pasteView()
}
ToolbarItem(placement: .topBarTrailing) {
Button {
newTournament = Tournament.newEmptyInstance()
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
if #available(iOS 26.0, *) {
Button("Ajouter", systemImage: "plus") {
newTournament = Tournament.newEmptyInstance()
}
} else {
Button {
newTournament = Tournament.newEmptyInstance()
} label: {
Image(systemName: "plus.circle.fill")
.resizable()
.scaledToFit()
.frame(minHeight: 32)
}
}
}
@ -296,6 +341,10 @@ struct ActivityView: View {
}
}
}
.sheet(isPresented: $presentOnboarding, content: {
OnboardingView()
.environmentObject(dataStore)
})
.sheet(isPresented: $presentFilterView) {
TournamentFilterView(federalDataViewModel: federalDataViewModel)
.environment(navigation)
@ -474,6 +523,11 @@ struct ActivityView: View {
navigation.agendaDestination = .tenup
}
SupportButtonView(contentIsUnavailable: true)
FooterButtonView("Vous n'êtes pas un juge-arbitre ou un organisateur de tournoi ? En savoir plus") {
presentOnboarding = true
}
.tint(.logoBackground)
}
}
@ -485,6 +539,7 @@ struct ActivityView: View {
}
}
@ViewBuilder
private func _tenupEmptyView() -> some View {
if dataStore.user.hasTenupClubs() == false {
ContentUnavailableView {
@ -496,6 +551,10 @@ struct ActivityView: View {
presentClubSearchView = true
}
.padding()
FooterButtonView("Cette app est dédié aux juge-arbitres et organisateurs de tournoi. Vous êtes un joueur à la recherche d'un tournoi homologué ? Utilisez notre outil de recherche") {
navigation.agendaDestination = .around
}
.tint(.logoBackground)
}
} else {
ContentUnavailableView {
@ -518,13 +577,16 @@ struct ActivityView: View {
ContentUnavailableView {
Label("Recherche de tournoi", systemImage: "magnifyingglass")
} description: {
Text("Chercher les tournois autour de vous pour mieux décider les tournois à proposer dans votre club. Padel Club vous facilite même l'inscription !")
Text("Chercher les tournois homologués autour de vous. Padel Club vous facilite même l'inscription !")
} actions: {
RowButtonView("Lancer la recherche") {
RowButtonView("Chercher un tournoi") {
displaySearchView = true
}
.padding()
}
.onAppear {
displaySearchView = true
}
} else {
if federalDataViewModel.lastError == nil {
ContentUnavailableView {

@ -30,7 +30,19 @@ struct TournamentLookUpView: View {
@State private var confirmSearch: Bool = false
@State private var locationRequested = false
@State private var apiError: StoreError?
@State private var quickOption: QuickDateOption? = nil
enum QuickDateOption: String, Identifiable, Hashable {
case thisMonth
case thisWeek
case nextWeek
case nextMonth
case twoWeeks
case nextThreeMonth
var id: String { self.rawValue }
}
var tournaments: [FederalTournament] {
federalDataViewModel.searchedFederalTournaments
}
@ -140,15 +152,21 @@ struct TournamentLookUpView: View {
}
.toolbarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Annuler", systemImage: "xmark", role: .cancel) {
dismiss()
}
}
ToolbarItem(placement: .bottomBar) {
if revealSearchParameters {
FooterButtonView("Lancer la recherche") {
Button("Lancer la recherche") {
if dataStore.appSettings.city.isEmpty {
confirmSearch = true
} else {
runSearch()
}
}
.buttonStyle(.borderedProminent)
.disabled(searching)
} else if searching {
HStack(spacing: 20) {
@ -197,7 +215,7 @@ struct TournamentLookUpView: View {
Text("Ré-initialiser la recherche")
}
} label: {
Label("Options", systemImage: "ellipsis.circle")
LabelOptions()
}
}
}
@ -253,6 +271,7 @@ struct TournamentLookUpView: View {
federalDataViewModel.searchedFederalTournaments = []
searching = true
requestedToGetAllPages = false
federalDataViewModel.weekdays = dataStore.appSettings.weekdays
federalDataViewModel.searchAttemptCount += 1
federalDataViewModel.dayPeriod = dataStore.appSettings.dayPeriod
federalDataViewModel.dayDuration = dataStore.appSettings.dayDuration
@ -340,6 +359,42 @@ struct TournamentLookUpView: View {
var searchParametersView: some View {
@Bindable var appSettings = dataStore.appSettings
Section {
Picker(selection: $quickOption) {
Text("Libre").tag(nil as QuickDateOption?)
Text("Cette semaine").tag(QuickDateOption.thisWeek as QuickDateOption?)
Text("2 prochaines semaines").tag(QuickDateOption.twoWeeks as QuickDateOption?)
Text("La semaine prochaine").tag(QuickDateOption.nextWeek as QuickDateOption?)
Text("Ce mois-ci").tag(QuickDateOption.thisMonth as QuickDateOption?)
Text("2 prochains mois").tag(QuickDateOption.nextMonth as QuickDateOption?)
Text("3 prochains mois").tag(QuickDateOption.nextThreeMonth as QuickDateOption?)
} label: {
Text("Choix de dates")
}
.pickerStyle(.menu)
.onChange(of: quickOption) { oldValue, newValue in
switch newValue {
case nil:
break
case .twoWeeks:
appSettings.startDate = Date().startOfDay
appSettings.endDate = Date().endOfWeek.addingTimeInterval(14 * 24 * 60 * 60)
case .nextWeek:
appSettings.startDate = Date().endOfWeek.nextDay.startOfDay
appSettings.endDate = Date().endOfWeek.addingTimeInterval(7 * 24 * 60 * 60)
case .thisMonth:
appSettings.startDate = Date().startOfDay
appSettings.endDate = Date().endOfMonth.endOfDay()
case .thisWeek:
appSettings.startDate = Date().startOfDay
appSettings.endDate = Date().endOfWeek
case .nextMonth:
appSettings.startDate = Date().startOfDay
appSettings.endDate = Date().endOfMonth.nextDay.endOfMonth
case .nextThreeMonth:
appSettings.startDate = Date().startOfDay
appSettings.endDate = Date().endOfMonth.nextDay.endOfMonth.nextDay.endOfMonth
}
}
DatePicker("Début", selection: $appSettings.startDate, displayedComponents: .date)
DatePicker("Fin", selection: $appSettings.endDate, displayedComponents: .date)
Picker(selection: $appSettings.dayDuration) {
@ -350,7 +405,9 @@ struct TournamentLookUpView: View {
} label: {
Text("Durée souhaitée (en jours)")
}
WeekdayselectionView(weekdays: $appSettings.weekdays)
Picker(selection: $appSettings.dayPeriod) {
ForEach(DayPeriod.allCases) {
Text($0.localizedDayPeriodLabel().capitalized).tag($0)
@ -398,6 +455,7 @@ struct TournamentLookUpView: View {
}
Picker(selection: $appSettings.distance) {
Text(distanceLimit(distance:15).formatted()).tag(15.0)
Text(distanceLimit(distance:30).formatted()).tag(30.0)
Text(distanceLimit(distance:50).formatted()).tag(50.0)
Text(distanceLimit(distance:60).formatted()).tag(60.0)

@ -0,0 +1,36 @@
//
// WeekdayselectionView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 24/09/2025.
//
import SwiftUI
import PadelClubData
import LeStorage
struct WeekdayselectionView: View {
@Binding var weekdays: Set<Int>
var body: some View {
NavigationLink {
List((1...7), selection: $weekdays) { type in
Text(Date.weekdays[type - 1]).tag(type as Int)
}
.navigationTitle("Jour de la semaine")
.environment(\.editMode, Binding.constant(EditMode.active))
} label: {
HStack {
Text("Jour de la semaine")
Spacer()
if weekdays.isEmpty || weekdays.count == 7 {
Text("N'importe")
.foregroundStyle(.secondary)
} else {
Text(weekdays.sorted().map({ Date.weekdays[$0 - 1] }).joined(separator: ", "))
.foregroundStyle(.secondary)
}
}
}
}
}

@ -19,7 +19,11 @@ struct MainView: View {
@Environment(ImportObserver.self) private var importObserver: ImportObserver
@State private var mainViewId: UUID = UUID()
@State private var presentOnboarding: Bool = false
@State private var canPresentOnboarding: Bool = false
@AppStorage("didSeeOnboarding") private var didSeeOnboarding: Bool = false
var lastDataSource: String? {
dataStore.appSettings.lastDataSource
}
@ -90,6 +94,17 @@ struct MainView: View {
// PadelClubView()
// .tabItem(for: .padelClub)
}
.onAppear {
if canPresentOnboarding || StoreCenter.main.userId != nil {
if didSeeOnboarding == false {
presentOnboarding = true
}
}
}
.sheet(isPresented: $presentOnboarding, content: {
OnboardingView()
.environmentObject(dataStore)
})
.id(mainViewId)
.onChange(of: dataStore.user.id) {
print("dataStore.user.id = ", dataStore.user.id)
@ -98,6 +113,8 @@ struct MainView: View {
navigation.path.removeLast(navigation.path.count)
mainViewId = UUID()
}
canPresentOnboarding = true
}
.environmentObject(dataStore)
.task {

@ -0,0 +1,235 @@
import SwiftUI
struct OnboardingView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@State private var selection = 0
@Environment(\.openURL) var openURL
@Environment(\.dismiss) private var dismiss
@AppStorage("didSeeOnboarding") private var didSeeOnboarding: Bool = false
var steps: [OnboardingStep] {
[
// Écran 1 Bienvenue
.single(
title: "Bienvenue sur Padel Club",
description: "L’outil idéal des juges-arbitres et organisateurs pour gérer leurs tournois de A à Z.",
image: .padelClubLogoFondclairTransparent,
imageSystem: nil,
buttonTitle: "Suivant",
action: { selection += 1 }
),
// Écran 2 Juges arbitres
.single(
title: "Pour les Juges-Arbitres",
description: "Planification, convocations, tirages, résultats… Tout ce qu’il faut pour organiser un tournoi de padel.",
image: nil,
imageSystem: "calendar.badge.clock",
buttonTitle: "Suivant",
action: { selection += 1 }
),
// Écran 3 Joueurs (Multi boutons)
.multi(
title: "Vous êtes joueur ?",
description: "Cette app a été pensée faite pour les organisateurs.\nPour suivre vos tournois et convocations, rendez-vous sur https://padelclub.app",
image: nil,
imageSystem: "person.fill.questionmark",
tools: [
("Aller sur le site joueur", {
if let url = URL(string: "https://padelclub.app") {
openURL(url)
}
})
],
finalButtonTitle: "Continuer",
finalAction: {
selection += 1
}
),
// Écran 4 Outils utiles aux joueurs
.multi(
title: "Quelques outils utiles",
description: "Même si pensée pour les organisateurs, vous trouverez aussi quelques fonctions pratiques en tant que joueur.",
image: nil,
imageSystem: "wrench.and.screwdriver",
tools: [
("Chercher un tournoi Ten'Up", {
dismiss()
navigation.agendaDestination = .around
}),
("Calculateur de points", {
dismiss()
navigation.selectedTab = .toolbox
}),
("Consulter les règles du jeu", {
dismiss()
navigation.selectedTab = .toolbox
}),
("Créer vos animations amicales", {
dismiss()
navigation.agendaDestination = .activity
})
],
finalButtonTitle: "J'ai compris",
finalAction: {
UserDefaults.standard.set(true, forKey: "didSeeOnboarding")
dismiss()
}
)
]
}
var body: some View {
NavigationStack {
TabView(selection: $selection) {
ForEach(Array(steps.enumerated()), id: \.offset) { index, step in
switch step {
case let .single(title, description, image, imageSystem, buttonTitle, action):
OnboardingPage(
title: title,
description: description,
image: image,
imageSystem: imageSystem,
buttonTitle: buttonTitle,
action: action
)
.tag(index)
case let .multi(title, description, image, imageSystem, tools, finalButtonTitle, finalAction):
OnboardingMultiButtonPage(
title: title,
description: description,
image: image,
imageSystem: imageSystem,
tools: tools,
finalButtonTitle: finalButtonTitle,
finalAction: finalAction
)
.tag(index)
}
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always)) // <- ensures background
.tint(.black) // <- sets the indicator color
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
didSeeOnboarding = true
dismiss()
} label: {
Text("Plus tard")
}
}
}
}
.tint(.master)
}
}
// MARK: - Enum de configuration
enum OnboardingStep {
case single(title: String, description: String, image: ImageResource?, imageSystem: String?, buttonTitle: String, action: () -> Void)
case multi(title: String, description: String, image: ImageResource?, imageSystem: String?, tools: [(String, () -> Void)], finalButtonTitle: String?, finalAction: () -> Void)
}
// MARK: - Vue de base commune
struct OnboardingBasePage<Content: View>: View {
var title: String
var description: String
var image: ImageResource?
var imageSystem: String?
@ViewBuilder var content: () -> Content
var body: some View {
VStack(spacing: 20) {
Spacer()
if let imageSystem {
Image(systemName: imageSystem)
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
} else if let image {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
}
Text(title)
.font(.title)
.fontWeight(.bold)
.multilineTextAlignment(.center)
Text(description)
.font(.body)
.multilineTextAlignment(.center)
.padding(.horizontal, 30)
.lineLimit(nil)
.fixedSize(horizontal: false, vertical: true)
Spacer()
content()
Spacer(minLength: 40)
}
}
}
// MARK: - Page avec un bouton
struct OnboardingPage: View {
var title: String
var description: String
var image: ImageResource?
var imageSystem: String?
var buttonTitle: String
var action: () -> Void
var body: some View {
OnboardingBasePage(title: title, description: description, image: image, imageSystem: imageSystem) {
RowButtonView(buttonTitle) {
action()
}
.padding()
}
}
}
// MARK: - Page avec plusieurs boutons
struct OnboardingMultiButtonPage: View {
var title: String
var description: String
var image: ImageResource?
var imageSystem: String?
var tools: [(String, () -> Void)]
var finalButtonTitle: String?
var finalAction: () -> Void
var body: some View {
OnboardingBasePage(title: title, description: description, image: image, imageSystem: imageSystem) {
VStack(spacing: 12) {
ForEach(Array(tools.enumerated()), id: \.offset) { _, tool in
FooterButtonView(tool.0) {
tool.1()
}
.tint(.master)
}
}
if let finalButtonTitle = finalButtonTitle {
RowButtonView(finalButtonTitle) {
finalAction()
}
.padding()
}
}
}
}
#Preview {
OnboardingView()
}

@ -139,13 +139,15 @@ struct EditScoreView: View {
Text(matchDescriptor.teamLabelTwo)
}
Divider()
Button {
self.matchDescriptor.match?.removeWalkOut()
save()
} label: {
Text("Annuler un forfait")
if self.matchDescriptor.match?.hasWalkoutTeam() == true {
Divider()
Button {
self.matchDescriptor.match?.removeWalkOut()
save()
} label: {
Text("Annuler un forfait")
}
}
} label: {
Text("Forfait d'une équipe ?")
@ -174,6 +176,13 @@ struct EditScoreView: View {
}
if matchDescriptor.hasEnded {
if self.matchDescriptor.match?.hasWalkoutTeam() == true {
RowButtonView("Annuler le forfait", role: .destructive) {
self.matchDescriptor.match?.removeWalkOut()
save()
}
}
Section {
HStack {
Spacer()

@ -88,7 +88,7 @@ struct FollowUpMatchView: View {
let allMatches = currentTournament?.allMatches() ?? []
self.matchesLeft = Tournament.matchesLeft(allMatches)
let runningMatches = Tournament.runningMatches(allMatches)
let readyMatches = Tournament.readyMatches(allMatches)
let readyMatches = Tournament.readyMatches(allMatches, runningMatches: runningMatches)
self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false)
self.isFree = currentTournament?.isFree() ?? true
}
@ -100,7 +100,7 @@ struct FollowUpMatchView: View {
self.autoDismiss = autoDismiss
self.matchesLeft = Tournament.matchesLeft(allMatches)
let runningMatches = Tournament.runningMatches(allMatches)
let readyMatches = Tournament.readyMatches(allMatches)
let readyMatches = Tournament.readyMatches(allMatches, runningMatches: runningMatches)
self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false)
self.isFree = false
}
@ -156,7 +156,7 @@ struct FollowUpMatchView: View {
case .index:
return matches
case .restingTime:
return matches.sorted(by: \.restingTimeForSorting)
return readyMatches.sorted(by: \.restingTimeForSorting)
case .court:
return matchesLeft.filter({ $0.courtIndex == selectedCourt })
case .winner:

@ -96,16 +96,27 @@ struct SelectablePlayerListView: View {
var body: some View {
VStack(spacing: 0) {
if importObserver.isImportingFile() == false {
if searchViewModel.filterSelectionEnabled == false {
VStack {
HStack {
Picker(selection: $searchViewModel.filterOption) {
ForEach(PlayerFilterOption.allCases, id: \.self) { scope in
Text(scope.icon().capitalized)
}
} label: {
VStack {
HStack {
Picker(selection: $searchViewModel.filterOption) {
ForEach(PlayerFilterOption.allCases, id: \.self) { scope in
Text(scope.icon().capitalized)
}
} label: {
}
.pickerStyle(.segmented)
Picker(selection: $searchViewModel.dataSet) {
ForEach(DataSet.allCases) { dataSet in
Text(searchViewModel.label(forDataSet: dataSet)).tag(dataSet)
}
.pickerStyle(.segmented)
} label: {
}
}
if searchViewModel.isPresented == false {
HStack {
Menu {
if let lastDataSource = dataStore.appSettings.localizedLastDataSource() {
Section {
@ -132,7 +143,7 @@ struct SelectablePlayerListView: View {
}
Divider()
Section {
Menu {
Picker(selection: $searchViewModel.selectedAgeCategory) {
ForEach(FederalTournamentAge.allCases) { ageCategory in
Text(ageCategory.localizedFederalAgeLabel(.title)).tag(ageCategory)
@ -141,11 +152,11 @@ struct SelectablePlayerListView: View {
Text("Catégorie d'âge")
}
} header: {
} label: {
Text("Catégorie d'âge")
}
Divider()
Section {
Toggle(isOn: .init(get: {
return searchViewModel.hideAssimilation == false
@ -165,23 +176,36 @@ struct SelectablePlayerListView: View {
Text("Assimilés")
}
} label: {
VStack(alignment: .trailing) {
Label(searchViewModel.sortOption.localizedLabel(), systemImage: searchViewModel.ascending ? "chevron.up" : "chevron.down")
if searchViewModel.selectedAgeCategory != .unlisted {
Text(searchViewModel.selectedAgeCategory.localizedFederalAgeLabel()).font(.caption)
}
Text("tri par " + searchViewModel.sortTitle().lowercased())
.underline()
.font(.caption)
// Label("Filtre", systemImage: "line.3.horizontal.decrease")
// .labelsHidden()
}
if searchViewModel.selectedPlayers.count > 0 {
Divider()
Button {
searchViewModel.filterSelectionEnabled.toggle()
} label: {
Text("\(searchViewModel.filterSelectionEnabled ? "masquer" : "voir") la sélection")
.underline()
.font(.caption)
}
}
}
.fixedSize()
}
.padding(.bottom)
.padding(.horizontal)
.background(Material.thick)
Divider()
}
.padding(.bottom)
.padding(.horizontal)
.background(Material.thick)
Divider()
MySearchView(searchViewModel: searchViewModel, contentUnavailableAction: contentUnavailableAction)
.environment(\.editMode, searchViewModel.allowMultipleSelection ? .constant(.active) : .constant(.inactive))
.searchable(text: $searchViewModel.debouncableText, tokens: $searchViewModel.tokens, suggestedTokens: $searchViewModel.suggestedTokens, isPresented: $searchViewModel.isPresented, placement: .navigationBarDrawer(displayMode: .always), prompt: searchViewModel.prompt(forDataSet: searchViewModel.dataSet), token: { token in
.searchable(text: $searchViewModel.debouncableText, tokens: $searchViewModel.tokens, suggestedTokens: $searchViewModel.suggestedTokens, isPresented: $searchViewModel.isPresented, placement: .toolbar, prompt: searchViewModel.prompt(forDataSet: searchViewModel.dataSet), token: { token in
Text(token.shortLocalizedLabel)
})
.keyboardType(.alphabet)
@ -212,11 +236,10 @@ struct SelectablePlayerListView: View {
}
.scrollDismissesKeyboard(.immediately)
.navigationBarBackButtonHidden(searchViewModel.allowMultipleSelection)
.toolbarBackground(searchViewModel.allowMultipleSelection ? .visible : .hidden, for: .bottomBar)
.toolbarBackground(.hidden, for: .bottomBar)
.toolbarBackground(.visible, for: .navigationBar)
// .toolbarRole(searchViewModel.allowMultipleSelection ? .navigationStack : .editor)
.interactiveDismissDisabled(searchViewModel.selectedPlayers.isEmpty == false)
.navigationTitle(searchViewModel.label(forDataSet: searchViewModel.dataSet))
.navigationBarTitleDisplayMode(.inline)
} else {
List {
@ -284,7 +307,7 @@ struct SelectablePlayerListView: View {
searchViewModel.selectedPlayers.removeAll()
dismiss()
} label: {
Text("Annuler")
Label("Annuler", systemImage: "xmark")
}
}
@ -297,28 +320,16 @@ struct SelectablePlayerListView: View {
}
.disabled(searchViewModel.selectedPlayers.isEmpty)
}
ToolbarItem(placement: .status) {
let count = searchViewModel.selectedPlayers.count
VStack(spacing: 0) {
Text(count.formatted() + " joueur" + count.pluralSuffix + " séléctionné" + count.pluralSuffix).font(.footnote).foregroundStyle(.secondary)
FooterButtonView("\(searchViewModel.filterSelectionEnabled ? "masquer" : "voir") la liste") {
searchViewModel.filterSelectionEnabled.toggle()
}
}
}
}
if #available(iOS 26.0, *) {
DefaultToolbarItem(kind: .search, placement: .bottomBar)
}
}
.navigationTitle("Recherche")
.navigationBarTitleDisplayMode(.large)
// .modifierWithCondition(searchViewModel.user != nil) { thisView in
// thisView
.toolbarTitleMenu {
Picker(selection: $searchViewModel.dataSet) {
ForEach(DataSet.allCases) { dataSet in
Text(searchViewModel.label(forDataSet: dataSet)).tag(dataSet)
}
} label: {
}
}
// }
// .bottomBarAlternative(hide: searchViewModel.selectedPlayers.isEmpty) {
// ZStack {

@ -47,6 +47,8 @@ struct TournamentFilterView: View {
} label: {
Text("En semaine ou week-end")
}
WeekdayselectionView(weekdays: $federalDataViewModel.weekdays)
}
Section {

@ -90,7 +90,7 @@ struct TeamRestingView: View {
let allMatches = tournament.allMatches()
let matchesLeft = Tournament.matchesLeft(allMatches)
let runningMatches = Tournament.runningMatches(allMatches)
let readyMatches = Tournament.readyMatches(allMatches)
let readyMatches = Tournament.readyMatches(allMatches, runningMatches: runningMatches)
self.readyMatches = Tournament.availableToStart(readyMatches, in: runningMatches, checkCanPlay: false)
self.matchesLeft = matchesLeft
self.teams = tournament.selectedSortedTeams().filter({ $0.restingTime() != nil }).sorted(by: \.restingTimeForSorting)

@ -336,7 +336,7 @@ struct InscriptionManagerView: View {
.tint(.master)
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
Toggle(isOn: $compactMode) {
Text("Vue compact")
@ -364,6 +364,14 @@ struct InscriptionManagerView: View {
LabelFilter()
.symbolVariant(filterMode == .all ? .none : .fill)
}
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .navigationBarTrailing)
}
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
if tournament.inscriptionClosed() == false {
Menu {

@ -201,7 +201,7 @@ struct PrintSettingsView: View {
Text("Partager le code source HTML")
}
} label: {
Label("Options", systemImage: "ellipsis.circle")
LabelOptions()
}
}
}

@ -42,7 +42,7 @@ struct TournamentRankView: View {
Section {
let all = tournament.allMatches()
let runningMatches = Tournament.runningMatches(all)
let matchesLeft = Tournament.readyMatches(all)
let matchesLeft = Tournament.readyMatches(all, runningMatches: runningMatches)
MatchListView(section: "Matchs restant", matches: matchesLeft, hideWhenEmpty: false, isExpanded: false)
MatchListView(section: "Matchs en cours", matches: runningMatches, hideWhenEmpty: false, isExpanded: false)

@ -22,7 +22,7 @@ struct TournamentRunningView: View {
let runningMatches = Tournament.runningMatches(allMatches)
let matchesLeft = Tournament.matchesLeft(allMatches)
let readyMatches = Tournament.readyMatches(allMatches)
let readyMatches = Tournament.readyMatches(allMatches, runningMatches: runningMatches)
let availableToStart = Tournament.availableToStart(allMatches, in: runningMatches, checkCanPlay: true)
Section {

@ -238,10 +238,10 @@ struct TournamentView: View {
}
NavigationLink(value: Screen.event) {
Text("Réglages de l'événement")
Label("Événement", systemImage: "wrench.and.screwdriver")
}
NavigationLink(value: Screen.settings) {
LabelSettings()
Label("Tournoi", systemImage: "wrench.and.screwdriver")
}
NavigationLink(value: Screen.call) {
@ -290,10 +290,10 @@ struct TournamentView: View {
}
}
NavigationLink(value: Screen.broadcast) {
Label("Publication", systemImage: "airplayvideo")
}
// NavigationLink(value: Screen.broadcast) {
// Label("Publication", systemImage: "airplayvideo")
// }
//
NavigationLink(value: Screen.print) {
Label("Imprimer", systemImage: "printer")
}
@ -305,8 +305,7 @@ struct TournamentView: View {
Divider()
NavigationLink(value: Screen.stateSettings) {
Text("Gestion du tournoi")
Text("Annuler, supprimer ou terminer le tournoi")
Label("Tournoi", systemImage: "trash")
}
} label: {
LabelOptions()

Loading…
Cancel
Save