overhaul screens disposition

main
Razmig Sarkissian 1 month ago
parent 2183f2863f
commit 8fdeff82f1
  1. 32
      PadelClub.xcodeproj/project.pbxproj
  2. 10
      PadelClub/PadelClubApp.swift
  3. 2
      PadelClub/ViewModel/NavigationViewModel.swift
  4. 5
      PadelClub/ViewModel/TabDestination.swift
  5. 47
      PadelClub/Views/Club/ClubsView.swift
  6. 3
      PadelClub/Views/GroupStage/GroupStageView.swift
  7. 2
      PadelClub/Views/Navigation/Agenda/ActivityView.swift
  8. 13
      PadelClub/Views/Navigation/MainView.swift
  9. 226
      PadelClub/Views/Navigation/MyAccount/MyAccountView.swift
  10. 104
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  11. 68
      PadelClub/Views/Navigation/Umpire/UmpireOptionsView.swift
  12. 85
      PadelClub/Views/Navigation/Umpire/UmpireSettingsView.swift
  13. 242
      PadelClub/Views/Navigation/Umpire/UmpireView.swift
  14. 3
      PadelClub/Views/Round/LoserRoundView.swift
  15. 1
      PadelClub/Views/Round/RoundView.swift
  16. 7
      PadelClub/Views/Shared/SupportButtonView.swift
  17. 23
      PadelClub/Views/Tournament/TournamentView.swift
  18. 55
      PadelClub/Views/User/AccountView.swift
  19. 2
      PadelClub/Views/User/LoginView.swift

@ -161,6 +161,15 @@
FF30ACF12E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; };
FF30ACF22E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; };
FF30ACF32E8D7078008B6006 /* PaymentRequestButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */; };
FF30AD302E92A994008B6006 /* MyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD2F2E92A994008B6006 /* MyAccountView.swift */; };
FF30AD312E92A994008B6006 /* MyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD2F2E92A994008B6006 /* MyAccountView.swift */; };
FF30AD322E92A994008B6006 /* MyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD2F2E92A994008B6006 /* MyAccountView.swift */; };
FF30AD342E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */; };
FF30AD352E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */; };
FF30AD362E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */; };
FF30AD3C2E93E822008B6006 /* UmpireSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */; };
FF30AD3D2E93E822008B6006 /* UmpireSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */; };
FF30AD3E2E93E822008B6006 /* UmpireSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */; };
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FF3795602B9396D0004EA093 /* PadelClubApp.xcdatamodeld */; };
FF3795662B9399AA004EA093 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3795652B9399AA004EA093 /* Persistence.swift */; };
FF39B6152DC8825E004E10CE /* PadelClubData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C49770202DC25A23005CD239 /* PadelClubData.framework */; };
@ -1030,6 +1039,9 @@
FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToAllView.swift; sourceTree = "<group>"; };
FF30ACEC2E8D700B008B6006 /* PaymentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentService.swift; sourceTree = "<group>"; };
FF30ACF02E8D7078008B6006 /* PaymentRequestButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentRequestButton.swift; sourceTree = "<group>"; };
FF30AD2F2E92A994008B6006 /* MyAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountView.swift; sourceTree = "<group>"; };
FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireOptionsView.swift; sourceTree = "<group>"; };
FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UmpireSettingsView.swift; sourceTree = "<group>"; };
FF3795612B9396D0004EA093 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = "<group>"; };
FF3795652B9399AA004EA093 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
FF39B60F2DC87FEB004E10CE /* PadelClubData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PadelClubData.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -1594,12 +1606,21 @@
path = SeedData;
sourceTree = "<group>";
};
FF30AD2E2E92A936008B6006 /* MyAccount */ = {
isa = PBXGroup;
children = (
FF30AD2F2E92A994008B6006 /* MyAccountView.swift */,
);
path = MyAccount;
sourceTree = "<group>";
};
FF39719B2B8DE04B004C4E75 /* Navigation */ = {
isa = PBXGroup;
children = (
FF59FFB62B90EFBF0061EFF9 /* MainView.swift */,
FFB0FF662E81B671009EDEAC /* OnboardingView.swift */,
FFD783FB2B91B919000F62A6 /* Agenda */,
FF30AD2E2E92A936008B6006 /* MyAccount */,
FF3F74FA2B91A04B004CFE0E /* Organizer */,
FF3F74FB2B91A060004CFE0E /* Toolbox */,
FF3F74FC2B91A06B004CFE0E /* Umpire */,
@ -1683,6 +1704,8 @@
isa = PBXGroup;
children = (
FF3F74F52B919E45004CFE0E /* UmpireView.swift */,
FF30AD3B2E93E822008B6006 /* UmpireSettingsView.swift */,
FF30AD332E93E5B4008B6006 /* UmpireOptionsView.swift */,
FFA252AC2CDB734A0074E63F /* UmpireStatisticView.swift */,
FFD783FE2B91BA42000F62A6 /* PadelClubView.swift */,
C488C8812CCBE8FC0082001F /* NetworkStatusView.swift */,
@ -2270,11 +2293,13 @@
FF1DC5572BAB3AED00FD8220 /* ClubsView.swift in Sources */,
FFE103122C366E5900684FC9 /* ImagePickerView.swift in Sources */,
FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */,
FF30AD312E92A994008B6006 /* MyAccountView.swift in Sources */,
FF967D062BAF3C4200A9A3BD /* MatchSetupView.swift in Sources */,
FF4AB6B52B9248200002987F /* NetworkManager.swift in Sources */,
FF2B6F5E2C036A1500835EE7 /* EventLinksView.swift in Sources */,
FF025AE12BD0EB9000A86CF8 /* TournamentClubSettingsView.swift in Sources */,
FFBF065C2BBD2657009D6715 /* GroupStageTeamView.swift in Sources */,
FF30AD362E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */,
FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */,
FFBE62052CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FFE2D2E22C231BEE00D0C7BE /* SupportButtonView.swift in Sources */,
@ -2440,6 +2465,7 @@
FF8E52342DF01D6100099B75 /* EventStatusView.swift in Sources */,
FFE8B63C2DACEAED00BDE966 /* ConfigurationService.swift in Sources */,
FF0E0B6D2BC254C6005F00A9 /* TournamentScheduleView.swift in Sources */,
FF30AD3D2E93E822008B6006 /* UmpireSettingsView.swift in Sources */,
FF025AF12BD1AEBD00A86CF8 /* MatchFormatStorageView.swift in Sources */,
FF3F74F62B919E45004CFE0E /* UmpireView.swift in Sources */,
FF967D012BAEF0B400A9A3BD /* MatchSummaryView.swift in Sources */,
@ -2542,11 +2568,13 @@
FF4CBF532C996C0600151637 /* ClubsView.swift in Sources */,
FF4CBF542C996C0600151637 /* ImagePickerView.swift in Sources */,
FF4CBF552C996C0600151637 /* MatchTypeSelectionView.swift in Sources */,
FF30AD302E92A994008B6006 /* MyAccountView.swift in Sources */,
FF4CBF562C996C0600151637 /* MatchSetupView.swift in Sources */,
FF4CBF572C996C0600151637 /* NetworkManager.swift in Sources */,
FF4CBF582C996C0600151637 /* EventLinksView.swift in Sources */,
FF4CBF5A2C996C0600151637 /* TournamentClubSettingsView.swift in Sources */,
FF4CBF5B2C996C0600151637 /* GroupStageTeamView.swift in Sources */,
FF30AD342E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */,
FF4CBF5C2C996C0600151637 /* RoundSettingsView.swift in Sources */,
FFBE62072CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FF4CBF5D2C996C0600151637 /* SupportButtonView.swift in Sources */,
@ -2712,6 +2740,7 @@
FF8E52362DF01D6100099B75 /* EventStatusView.swift in Sources */,
FF4CBFFB2C996C0600151637 /* MatchFormatStorageView.swift in Sources */,
FF4CBFFC2C996C0600151637 /* UmpireView.swift in Sources */,
FF30AD3C2E93E822008B6006 /* UmpireSettingsView.swift in Sources */,
FF4CBFFE2C996C0600151637 /* MatchSummaryView.swift in Sources */,
FFE8B5B52DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */,
FFA252B52CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,
@ -2792,11 +2821,13 @@
FF70FAD22C90584900129CC2 /* ClubsView.swift in Sources */,
FF70FAD32C90584900129CC2 /* ImagePickerView.swift in Sources */,
FF70FAD42C90584900129CC2 /* MatchTypeSelectionView.swift in Sources */,
FF30AD322E92A994008B6006 /* MyAccountView.swift in Sources */,
FF70FAD52C90584900129CC2 /* MatchSetupView.swift in Sources */,
FF70FAD62C90584900129CC2 /* NetworkManager.swift in Sources */,
FF70FAD72C90584900129CC2 /* EventLinksView.swift in Sources */,
FF70FAD92C90584900129CC2 /* TournamentClubSettingsView.swift in Sources */,
FF70FADA2C90584900129CC2 /* GroupStageTeamView.swift in Sources */,
FF30AD352E93E5B4008B6006 /* UmpireOptionsView.swift in Sources */,
FF70FADB2C90584900129CC2 /* RoundSettingsView.swift in Sources */,
FFBE62062CE9DA0900815D33 /* MatchViewStyle.swift in Sources */,
FF70FADC2C90584900129CC2 /* SupportButtonView.swift in Sources */,
@ -2962,6 +2993,7 @@
FF8E52352DF01D6100099B75 /* EventStatusView.swift in Sources */,
FF70FB7A2C90584900129CC2 /* MatchFormatStorageView.swift in Sources */,
FF70FB7B2C90584900129CC2 /* UmpireView.swift in Sources */,
FF30AD3E2E93E822008B6006 /* UmpireSettingsView.swift in Sources */,
FF70FB7D2C90584900129CC2 /* MatchSummaryView.swift in Sources */,
FFE8B5B42DA848D400BDE966 /* OnlineWaitingListFaqSheetView.swift in Sources */,
FFA252B72CDD2C6C0074E63F /* OngoingDestination.swift in Sources */,

@ -193,11 +193,11 @@ struct PadelClubApp: App {
navigationViewModel.selectedTab = .umpire
}
if navigationViewModel.umpirePath.isEmpty {
navigationViewModel.umpirePath.append(UmpireView.UmpireScreen.login)
} else if navigationViewModel.umpirePath.last! != .login {
navigationViewModel.umpirePath.removeAll()
navigationViewModel.umpirePath.append(UmpireView.UmpireScreen.login)
if navigationViewModel.accountPath.isEmpty {
navigationViewModel.accountPath.append(MyAccountView.AccountScreen.login)
} else if navigationViewModel.accountPath.last! != .login {
navigationViewModel.accountPath.removeAll()
navigationViewModel.accountPath.append(MyAccountView.AccountScreen.login)
}
}
}.resume()

@ -12,7 +12,7 @@ import PadelClubData
class NavigationViewModel {
var path = NavigationPath()
var toolboxPath = NavigationPath()
var umpirePath: [UmpireView.UmpireScreen] = []
var accountPath: [MyAccountView.AccountScreen] = []
var ongoingPath = NavigationPath()
var selectedTab: TabDestination?
var agendaDestination: AgendaDestination? = .activity

@ -17,6 +17,7 @@ enum TabDestination: CaseIterable, Identifiable {
case tournamentOrganizer
case umpire
case ongoing
case myAccount
var title: String {
switch self {
@ -30,6 +31,8 @@ enum TabDestination: CaseIterable, Identifiable {
return "Gestionnaire"
case .umpire:
return "Juge-Arbitre"
case .myAccount:
return "Compte"
}
}
@ -45,6 +48,8 @@ enum TabDestination: CaseIterable, Identifiable {
return "squares.below.rectangle"
case .umpire:
return "person.bust"
case .myAccount:
return "person.crop.circle"
}
}
}

@ -28,19 +28,19 @@ struct ClubsView: View {
var body: some View {
List {
#if DEBUG
Section {
RowButtonView("Delete unexisted clubs", action: {
let ids = dataStore.user.clubs
ids.forEach { clubId in
if dataStore.clubs.findById(clubId) == nil {
dataStore.user.clubs.removeAll(where: { $0 == clubId })
}
}
dataStore.saveUser()
})
}
#endif
// #if DEBUG
// Section {
// RowButtonView("Delete unexisted clubs", action: {
// let ids = dataStore.user.clubs
// ids.forEach { clubId in
// if dataStore.clubs.findById(clubId) == nil {
// dataStore.user.clubs.removeAll(where: { $0 == clubId })
// }
// }
// dataStore.saveUser()
// })
// }
// #endif
let clubs : [Club] = dataStore.user.clubsObjects(includeCreated: false)
let onlyCreatedClubs : [Club] = dataStore.user.createdClubsObjectsNotFavorite()
@ -106,7 +106,6 @@ struct ClubsView: View {
}
}
.navigationTitle(selection == nil ? "Clubs favoris" : "Choisir un club")
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.sheet(isPresented: presentClubCreationView) {
if let newClub {
@ -129,7 +128,12 @@ struct ClubsView: View {
.tint(.master)
}
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
ToolbarItem(placement: .topBarTrailing) {
if #available(iOS 26.0, *) {
Button("Chercher", systemImage: "magnifyingglass") {
presentClubSearchView = true
}
} else {
Button {
presentClubSearchView = true
} label: {
@ -138,7 +142,19 @@ struct ClubsView: View {
.scaledToFit()
.frame(minHeight: 28)
}
}
}
if #available(iOS 26.0, *) {
ToolbarSpacer(placement: .topBarTrailing)
}
ToolbarItem(placement: .topBarTrailing) {
if #available(iOS 26.0, *) {
Button("Ajouter", systemImage: "plus") {
newClub = Club.newEmptyInstance()
}
} else {
Button {
newClub = Club.newEmptyInstance()
} label: {
@ -150,6 +166,7 @@ struct ClubsView: View {
}
}
}
}
private func _contentUnavailable() -> some View {
ContentUnavailableView {

@ -96,6 +96,9 @@ struct GroupStageView: View {
}
}
.onAppear(perform: {
groupStage.clearScoreCache()
})
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
_groupStageMenuView()

@ -546,7 +546,7 @@ struct ActivityView: View {
.frame(width: 100)
}
} description: {
Text("Aucun événement en cours ou à venir dans votre agenda.")
Text("Aucun événement dans votre agenda.")
} actions: {
RowButtonView("Créer un nouvel événement") {
newTournament = Tournament.newEmptyInstance()

@ -80,9 +80,12 @@ struct MainView: View {
}
.toolbarBackground(.visible, for: .tabBar)
TournamentOrganizerView()
.tabItem(for: .tournamentOrganizer)
UmpireOptionsView()
.tabItem(for: .umpire)
.toolbarBackground(.visible, for: .tabBar)
// TournamentOrganizerView()
// .tabItem(for: .tournamentOrganizer)
// .toolbarBackground(.visible, for: .tabBar)
OngoingContainerView()
.tabItem(for: .ongoing)
.badge(self.dataStore.runningMatches().count)
@ -90,10 +93,10 @@ struct MainView: View {
ToolboxView()
.tabItem(for: .toolbox)
.toolbarBackground(.visible, for: .tabBar)
UmpireView()
.tabItem(for: .umpire)
.badge(badgeText)
MyAccountView()
.tabItem(for: .myAccount)
.toolbarBackground(.visible, for: .tabBar)
.badge(badgeText)
// PadelClubView()
// .tabItem(for: .padelClub)
}

@ -0,0 +1,226 @@
//
// MyAccountView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 01/03/2024.
//
import SwiftUI
import CoreLocation
import LeStorage
import StoreKit
import PadelClubData
struct MyAccountView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@EnvironmentObject var dataStore: DataStore
@State private var showSubscriptions: Bool = false
@State private var showProductIds: Bool = false
@FocusState private var focusedField: CustomUser.CodingKeys?
// @State var isConnected: Bool = false
enum AccountScreen {
case login
}
var body: some View {
@Bindable var navigation = navigation
NavigationStack(path: $navigation.accountPath) {
List {
Section {
SupportButtonView(supportButtonType: .bugReport, showIcon: true)
}
PurchaseListView()
Section {
Button {
self.showSubscriptions = true
} label: {
Label("Les offres", systemImage: "bookmark.fill")
}.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
self.showProductIds = true
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
self.showSubscriptions = true
}
)
}
if StoreCenter.main.isAuthenticated {
NavigationLink {
AccountView(user: dataStore.user) { }
} label: {
AccountRowView(userName: dataStore.user.username)
}
} else {
NavigationLink(value: AccountScreen.login) {
AccountRowView(userName: dataStore.user.username)
}
}
if StoreCenter.main.isAuthenticated {
let onlineRegPaymentMode = dataStore.user.registrationPaymentMode
Section {
LabeledContent {
switch onlineRegPaymentMode {
case .corporate:
Text("Activé")
.bold()
.foregroundStyle(.green)
case .disabled:
Text("Désactivé")
.bold()
case .noFee:
Text("Activé")
.bold()
.foregroundStyle(.green)
case .stripe:
Text("Activé")
.bold()
.foregroundStyle(.green)
}
} label: {
Text("Option 'Paiement en ligne'")
if onlineRegPaymentMode == .corporate {
Text("Mode Padel Club")
.foregroundStyle(.secondary)
} else if onlineRegPaymentMode == .noFee {
Text("Commission Stripe")
.foregroundStyle(.secondary)
} else if onlineRegPaymentMode == .stripe {
Text("Commission Stripe et Padel Club")
.foregroundStyle(.secondary)
}
}
} footer: {
if onlineRegPaymentMode == .disabled {
FooterButtonView("Contactez nous pour activer cette option.") {
let emailTo: String = "support@padelclub.app"
let subject: String = "Activer l'option de paiment en ligne : \(dataStore.user.email)"
if let url = URL(string: "mailto:\(emailTo)?subject=\(subject)"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
.font(.callout)
.multilineTextAlignment(.leading)
} else {
Text("Permet de proposer le paiement de vos tournois en ligne.")
}
}
Section {
SupportButtonView(supportButtonType: .sharingRequest)
} header: {
Text("Partage et délégation de compte")
} footer: {
Text("Vous souhaitez partager la supervision d'un tournoi à un autre compte ? Vous avez plusieurs juge-arbitres dans votre club ?")
}
}
Section {
Link(destination: URLs.appReview.url) {
Text("Partagez vos impressions !")
}
Link(destination: URLs.instagram.url) {
Text("Compte Instagram PadelClub.app")
}
Link(destination: URLs.appDescription.url) {
Text("Page de présentation de Padel Club")
}
}
Section {
Link(destination: URLs.privacy.url) {
Text("Politique de confidentialité")
}
Link(destination: URLs.eula.url) {
Text("Contrat d'utilisation")
}
}
}
.sheet(isPresented: self.$showSubscriptions, content: {
NavigationStack {
SubscriptionView(isPresented: self.$showSubscriptions)
.environment(\.colorScheme, .light)
}
})
.sheet(isPresented: self.$showProductIds, content: {
ProductIdsView()
})
.navigationDestination(for: AccountScreen.self) { screen in
switch screen {
case .login:
LoginView {_ in }
}
}
.navigationTitle("Mon compte")
}
}
}
struct AccountRowView: View {
@EnvironmentObject var dataStore: DataStore
var userName: String
var body: some View {
let isAuthenticated = StoreCenter.main.isAuthenticated
LabeledContent {
if isAuthenticated {
Text(self.userName)
} else if StoreCenter.main.userName != nil {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.logoRed)
}
} label: {
Label("Mon compte", systemImage: "person.fill")
if isAuthenticated && dataStore.user.email.isEmpty == false {
Text(dataStore.user.email)
}
}
}
}
struct ProductIdsView: View {
@State var transactions: [StoreKit.Transaction] = []
var body: some View {
VStack {
List {
LabeledContent("count", value: String(self.transactions.count))
ForEach(self.transactions) { transaction in
if #available(iOS 17.2, *) {
if let offer = transaction.offer {
LabeledContent(transaction.productID, value: "\(offer.type)")
} else {
LabeledContent(transaction.productID, value: "no offer")
}
} else {
Text("need ios 17.2")
}
}
}.onAppear {
Task {
self.transactions = Array(Guard.main.purchasedTransactions)
}
}
}
}
}

@ -40,26 +40,32 @@ struct ToolboxView: View {
NavigationStack(path: $navigation.toolboxPath) {
List {
Section {
Link(destination: URLs.main.url) {
Text("Accéder à padelclub.app")
NavigationLink {
PadelClubView()
} label: {
if let _lastDataSourceDate {
LabeledContent {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
} label: {
Label(_lastDataSourceDate.monthYearFormatted, systemImage: "calendar.badge.checkmark")
}
.contextMenu {
ShareLink(item: URLs.main.url)
} else {
LabeledContent {
Image(systemName: "xmark.circle.fill")
.tint(.logoRed)
} label: {
if let _mostRecentDateAvailable {
Label(_mostRecentDateAvailable.monthYearFormatted, systemImage: "calendar.badge")
} else {
Label("Aucun", systemImage: "calendar.badge.exclamationmark")
}
SupportButtonView(supportButtonType: .bugReport)
Link(destination: URLs.appReview.url) {
Text("Partagez vos impressions !")
Text("Classement mensuel disponible")
}
Link(destination: URLs.instagram.url) {
Text("Compte Instagram PadelClub.app")
}
}
if self.showDebugViews {
DebugView()
} header: {
Text("Classement mensuel utilisé")
}
Section {
@ -78,30 +84,15 @@ struct ToolboxView: View {
}
Section {
NavigationLink {
PadelClubView()
} label: {
if let _lastDataSourceDate {
LabeledContent {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
} label: {
Text(_lastDataSourceDate.monthYearFormatted)
Text("Classement mensuel utilisé")
}
} else {
LabeledContent {
Image(systemName: "xmark.circle.fill")
.tint(.logoRed)
} label: {
if let _mostRecentDateAvailable {
Text(_mostRecentDateAvailable.monthYearFormatted)
} else {
Text("Aucun")
}
Text("Classement mensuel disponible")
Link(destination: URLs.main.url) {
Label("Padel Club sur le Web", systemImage: "link")
}
.contextMenu {
ShareLink(item: URLs.main.url)
}
ShareLink(item: URLs.appStore.url) {
Label("Padel Club sur l'App Store", systemImage: "link")
}
}
@ -119,19 +110,8 @@ struct ToolboxView: View {
}
}
Section {
Link(destination: URLs.appDescription.url) {
Text("Page de présentation de Padel Club")
}
}
Section {
Link(destination: URLs.privacy.url) {
Text("Politique de confidentialité")
}
Link(destination: URLs.eula.url) {
Text("Contrat d'utilisation")
}
if self.showDebugViews {
DebugView()
}
Section {
@ -163,16 +143,9 @@ struct ToolboxView: View {
}
}
}
// .navigationBarTitleDisplayMode(.large)
// .navigationTitle(TabDestination.toolbox.title)
.navigationBarTitleDisplayMode(.large)
.navigationTitle(TabDestination.toolbox.title)
.toolbar {
ToolbarItem(placement: .principal) {
Text(TabDestination.toolbox.title)
.font(.headline)
.onTapGesture {
_handleTitleTap()
}
}
ToolbarItem(placement: .topBarLeading) {
Link(destination: URLs.appStore.url) {
Text("v\(PadelClubApp.appVersion)")
@ -180,15 +153,16 @@ struct ToolboxView: View {
}
ToolbarItem(placement: .topBarTrailing) {
Menu {
ShareLink(item: URLs.appStore.url) {
Label("Lien AppStore", systemImage: "link")
}
ShareLink(item: ZipLog(), preview: .init("Mon archive")) {
Label("Mes données", systemImage: "server.rack")
Text("Archiver mes données")
}
Divider()
Toggle("Outils avancées", isOn: $showDebugViews)
} label: {
Label("Partagez", systemImage: "square.and.arrow.up").labelStyle(.iconOnly)
LabelOptions()
}
}
}

@ -0,0 +1,68 @@
//
// UmpireOptionsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 06/10/2025.
//
import SwiftUI
import PadelClubData
struct UmpireOptionsView: View {
@Environment(NavigationViewModel.self) private var navigation: NavigationViewModel
@State private var umpireOption: UmpireOption? = .umpire
var body: some View {
@Bindable var navigation = navigation
NavigationStack {
VStack(spacing: 0) {
GenericDestinationPickerView(selectedDestination: $umpireOption, destinations: UmpireOption.allCases, nilDestinationIsValid: true)
switch umpireOption {
case .none:
UmpireSettingsView()
.navigationTitle("Préférences")
case .umpire:
UmpireView()
.navigationTitle("Juge-Arbitre")
case .clubs:
ClubsView()
}
}
.navigationBarTitleDisplayMode(.large)
.navigationTitle("Juge-Arbitre")
.toolbarBackground(.visible, for: .navigationBar)
}
}
}
enum UmpireOption: Int, CaseIterable, Identifiable, Selectable, Equatable {
func badgeValue() -> Int? {
nil
}
func badgeImage() -> PadelClubData.Badge? {
nil
}
func badgeValueColor() -> Color? {
nil
}
var id: Int { self.rawValue }
case umpire
case clubs
var localizedTitleKey: String {
switch self {
case .umpire:
return "Juge-Arbitre"
case .clubs:
return "Clubs Favoris"
}
}
func selectionLabel(index: Int) -> String {
localizedTitleKey
}
}

@ -0,0 +1,85 @@
//
// UmpireSettingsView.swift
// PadelClub
//
// Created by Razmig Sarkissian on 06/10/2025.
//
import SwiftUI
import CoreLocation
import LeStorage
import StoreKit
import PadelClubData
struct UmpireSettingsView: View {
@EnvironmentObject var dataStore: DataStore
var body: some View {
List {
if dataStore.user.canEnableOnlinePayment() {
Section {
if let tournamentTemplate = Tournament.getTemplateTournament() {
NavigationLink {
RegistrationSetupView(tournament: tournamentTemplate)
} label: {
Text("Référence")
Text(tournamentTemplate.tournamentTitle()).foregroundStyle(.secondary)
}
} else {
Text("Aucun tournoi référence. Choisissez-en un dans la liste d'activité")
}
} header: {
Text("Inscription et paiement en ligne")
} footer: {
Text("Tournoi référence utilisé pour les réglages des inscriptions en ligne")
}
}
Section {
@Bindable var user = dataStore.user
Toggle(isOn: $user.disableRankingFederalRuling) {
Text("Désactiver la règle fédéral")
}
.onChange(of: user.disableRankingFederalRuling) {
dataStore.saveUser()
}
} header: {
Text("Règle fédérale classement finale")
} footer: {
Text("Dernier de poule ≠ dernier du tournoi")
}
Section {
@Bindable var user = dataStore.user
Picker(selection: $user.loserBracketMode) {
ForEach(LoserBracketMode.allCases) {
Text($0.localizedLoserBracketMode()).tag($0)
}
} label: {
Text("Position des perdants")
}
.onChange(of: user.loserBracketMode) {
dataStore.saveUser()
}
} header: {
Text("Matchs de classement")
}
Section {
NavigationLink {
GlobalSettingsView()
} label: {
Label("Formats de jeu par défaut", systemImage: "megaphone")
}
NavigationLink {
DurationSettingsView()
} label: {
Label("Définir les durées moyennes", systemImage: "deskclock")
}
} footer: {
Text("Vous pouvez définir vos propres estimations de durées de match en fonction du format de jeu.")
}
}
}
}

@ -17,8 +17,6 @@ struct UmpireView: View {
@EnvironmentObject var dataStore: DataStore
@State private var presentSearchView: Bool = false
@State private var showSubscriptions: Bool = false
@State private var showProductIds: Bool = false
@State private var umpireCustomMail: String
@State private var umpireCustomPhone: String
@State private var umpireCustomContact: String
@ -38,50 +36,9 @@ struct UmpireView: View {
_umpireCustomContact = State(wrappedValue: DataStore.shared.user.umpireCustomContact ?? "")
}
enum UmpireScreen {
case login
}
var body: some View {
@Bindable var navigation = navigation
NavigationStack(path: $navigation.umpirePath) {
List {
PurchaseListView()
Section {
Button {
self.showSubscriptions = true
} label: {
Label("Les offres", systemImage: "bookmark.fill")
}.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
self.showProductIds = true
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
self.showSubscriptions = true
}
)
}
if StoreCenter.main.isAuthenticated {
NavigationLink {
AccountView(user: dataStore.user) { }
} label: {
AccountRowView(userName: dataStore.user.username)
}
} else {
NavigationLink(value: UmpireScreen.login) {
AccountRowView(userName: dataStore.user.username)
}
}
let currentPlayerData = dataStore.user.currentPlayerData()
Section {
if let reason = licenseMessage {
@ -106,6 +63,8 @@ struct UmpireView: View {
.autocorrectionDisabled()
.frame(maxWidth: .infinity)
}
} header: {
Text("Mes infos licencié")
} footer: {
if dataStore.user.licenceId == nil {
Text("Si vous avez participé à un tournoi dans les 12 derniers mois, Padel Club peut vous retrouver.")
@ -132,30 +91,6 @@ struct UmpireView: View {
}
}
}
Section {
NavigationLink {
ClubsView()
} label: {
LabeledContent {
Text(dataStore.user.clubs.count.formatted())
} label: {
Label("Clubs favoris", systemImage: "house.and.flag")
}
}
} footer: {
Text("Il s'agit des clubs qui sont utilisés pour récupérer les tournois tenup.")
}
// Section {
// NavigationLink {
// UmpireStatisticView()
// } label: {
// Text("Statistiques de participations")
// }
// }
//
if StoreCenter.main.isAuthenticated {
_customUmpireView()
Section {
@ -175,98 +110,26 @@ struct UmpireView: View {
Text("Ces informations ne seront pas affichées sur la page d'information des tournois sur Padel Club et dans les emails envoyés automatiquement au regard des inscriptions en lignes.")
}
}
if dataStore.user.canEnableOnlinePayment() {
Section {
if let tournamentTemplate = Tournament.getTemplateTournament() {
NavigationLink {
RegistrationSetupView(tournament: tournamentTemplate)
} label: {
Text("Référence")
Text(tournamentTemplate.tournamentTitle()).foregroundStyle(.secondary)
}
} else {
Text("Aucun tournoi référence. Choisissez-en un dans la liste d'activité")
}
} header: {
Text("Inscription et paiement en ligne")
} footer: {
Text("Tournoi référence utilisé pour les réglages des inscriptions en ligne")
}
}
Section {
@Bindable var user = dataStore.user
Toggle(isOn: $user.disableRankingFederalRuling) {
Text("Désactiver la règle fédéral")
}
.onChange(of: user.disableRankingFederalRuling) {
dataStore.saveUser()
}
} header: {
Text("Règle fédérale classement finale")
} footer: {
Text("Dernier de poule ≠ dernier du tournoi")
}
Section {
@Bindable var user = dataStore.user
Picker(selection: $user.loserBracketMode) {
ForEach(LoserBracketMode.allCases) {
Text($0.localizedLoserBracketMode()).tag($0)
}
} label: {
Text("Position des perdants")
}
.onChange(of: user.loserBracketMode) {
dataStore.saveUser()
}
} header: {
Text("Matchs de classement")
}
Section {
NavigationLink {
GlobalSettingsView()
} label: {
Label("Formats de jeu par défaut", systemImage: "megaphone")
}
NavigationLink {
DurationSettingsView()
} label: {
Label("Définir les durées moyennes", systemImage: "deskclock")
.overlay(content: {
if StoreCenter.main.isAuthenticated == false {
ContentUnavailableView {
Label("Aucun compte", systemImage: "person.crop.circle.badge.exclamationmark")
} description: {
Text("Créer un compte Padel Club pour personnaliser vos informations de Juge-Arbitre")
} actions: {
RowButtonView("Créer un compte") {
_openCreateAccountView()
}
} footer: {
Text("Vous pouvez définir vos propres estimations de durées de match en fonction du format de jeu.")
}
// Section {
// Text("Tenup ID")
// }
//
// Section {
// Text("Tournois")
// }
//
// Section {
// NavigationLink {
//
// } label: {
// Text("Favori")
// }
// NavigationLink {
//
// } label: {
// Text("Black list")
// }
// }
}
})
.onChange(of: StoreCenter.main.userId) {
license = dataStore.user.licenceId ?? ""
licenseMessage = nil
}
.navigationTitle("Juge-Arbitre")
.navigationBarBackButtonHidden(focusedField != nil)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar(content: {
if focusedField != nil {
ToolbarItem(placement: .topBarLeading) {
@ -320,15 +183,6 @@ struct UmpireView: View {
_confirmlicense()
}
}
.sheet(isPresented: self.$showSubscriptions, content: {
NavigationStack {
SubscriptionView(isPresented: self.$showSubscriptions)
.environment(\.colorScheme, .light)
}
})
.sheet(isPresented: self.$showProductIds, content: {
ProductIdsView()
})
.sheet(isPresented: $presentSearchView) {
let user = dataStore.user
NavigationStack {
@ -353,13 +207,6 @@ struct UmpireView: View {
}
}
}
.navigationDestination(for: UmpireScreen.self) { screen in
switch screen {
case .login:
LoginView {_ in }
}
}
}
}
private func _confirmlicense() {
@ -374,6 +221,10 @@ struct UmpireView: View {
}
private func _openCreateAccountView() {
navigation.selectedTab = .myAccount
}
private func _updateUserLicense(license: String?) {
guard let license else { return }
@ -481,67 +332,10 @@ struct UmpireView: View {
} }
} header: {
Text("Juge-arbitre")
Text("Mes infos juge-arbitre")
} footer: {
Text("Par défaut, les informations de Tenup sont récupérés, et si ce n'est pas le cas, ces informations seront utilisées pour vous contacter. Vous pouvez les modifier si vous souhaitez utiliser les informations de contact différentes de votre compte Padel Club.")
}
}
}
struct AccountRowView: View {
@EnvironmentObject var dataStore: DataStore
var userName: String
var body: some View {
let isAuthenticated = StoreCenter.main.isAuthenticated
LabeledContent {
if isAuthenticated {
Text(self.userName)
} else if StoreCenter.main.userName != nil {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.logoRed)
}
} label: {
Label("Mon compte", systemImage: "person.fill")
if isAuthenticated && dataStore.user.email.isEmpty == false {
Text(dataStore.user.email)
}
}
}
}
struct ProductIdsView: View {
@State var transactions: [StoreKit.Transaction] = []
var body: some View {
VStack {
List {
LabeledContent("count", value: String(self.transactions.count))
ForEach(self.transactions) { transaction in
if #available(iOS 17.2, *) {
if let offer = transaction.offer {
LabeledContent(transaction.productID, value: "\(offer.type)")
} else {
LabeledContent(transaction.productID, value: "no offer")
}
} else {
Text("need ios 17.2")
}
}
}.onAppear {
Task {
self.transactions = Array(Guard.main.purchasedTransactions)
}
}
}
}
}
//#Preview {
// UmpireView()
//}

@ -103,6 +103,9 @@ struct LoserRoundView: View {
}
.onAppear(perform: {
updateDisplayedMatches()
self.loserBracket.rounds.forEach({ round in
round.invalidateCache()
})
})
.onChange(of: isEditingTournamentSeed.wrappedValue) {
updateDisplayedMatches()

@ -29,6 +29,7 @@ struct RoundView: View {
func _refreshRound() {
self.upperRound.playedMatches = self.upperRound.round.playedMatches()
self.upperRound.round.invalidateCache()
}
init(upperRound: UpperRound) {

@ -90,11 +90,18 @@ struct SupportButtonView: View {
_zip()
}
case .bugReport:
if showIcon {
Button("Signaler un problème", systemImage: "square.and.pencil") {
_zip()
}
.labelStyle(.titleAndIcon)
} else {
Button("Signaler un problème") {
_zip()
}
}
}
}
.alert("Un problème est survenu", isPresented: messageSentFailed) {
Button("OK") {
}

@ -236,14 +236,14 @@ struct TournamentView: View {
}
#endif
if presentationContext == .agenda {
Button {
navigation.openTournamentInOrganizer(tournament)
} label: {
Label("Gestionnaire", systemImage: "pin")
}
Divider()
}
// if presentationContext == .agenda {
// Button {
// navigation.openTournamentInOrganizer(tournament)
// } label: {
// Label("Gestionnaire", systemImage: "pin")
// }
// Divider()
// }
NavigationLink(value: Screen.event) {
Label("Événement", systemImage: "info")
@ -277,10 +277,9 @@ struct TournamentView: View {
Label(tournament.isFree() ? "Présence" : "Encaissement", systemImage: tournament.isFree() ? "person.crop.circle.badge.checkmark" : "eurosign.circle")
}
// NavigationLink(value: Screen.statistics) {
// Label("Statistiques", systemImage: "123.rectangle")
// }
//
NavigationLink(value: Screen.statistics) {
Label("Statistiques", systemImage: "123.rectangle")
}
NavigationLink(value: Screen.rankings) {
LabeledContent {

@ -24,61 +24,6 @@ struct AccountView: View {
PurchaseView(purchaseRow: PurchaseRow(id: purchase.id, name: purchase.productId, item: item))
}
#endif
let onlineRegPaymentMode = dataStore.user.registrationPaymentMode
Section {
LabeledContent {
switch onlineRegPaymentMode {
case .corporate:
Text("Activé")
.bold()
.foregroundStyle(.green)
case .disabled:
Text("Désactivé")
.bold()
case .noFee:
Text("Activé")
.bold()
.foregroundStyle(.green)
case .stripe:
Text("Activé")
.bold()
.foregroundStyle(.green)
}
} label: {
Text("Option 'Paiement en ligne'")
if onlineRegPaymentMode == .corporate {
Text("Mode Padel Club")
.foregroundStyle(.secondary)
} else if onlineRegPaymentMode == .noFee {
Text("Commission Stripe")
.foregroundStyle(.secondary)
} else if onlineRegPaymentMode == .stripe {
Text("Commission Stripe et Padel Club")
.foregroundStyle(.secondary)
}
}
} footer: {
if onlineRegPaymentMode == .disabled {
FooterButtonView("Contactez nous pour activer cette option.") {
let emailTo: String = "support@padelclub.app"
let subject: String = "Activer l'option de paiment en ligne : \(dataStore.user.email)"
if let url = URL(string: "mailto:\(emailTo)?subject=\(subject)"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
.font(.callout)
.multilineTextAlignment(.leading)
} else {
Text("Permet de proposer le paiement de vos tournois en ligne.")
}
}
Section {
Text("Vous souhaitez partager la supervision d'un tournoi à un autre compte ? Vous avez plusieurs juge-arbitres dans votre club ?")
SupportButtonView(supportButtonType: .sharingRequest)
}
Section {
NavigationLink("Changer de mot de passe") {
ChangePasswordView()

@ -207,7 +207,7 @@ struct LoginView: View {
dataStore.appSettingsStorage.write()
}
self.handler(user)
navigation.umpirePath.removeAll()
navigation.accountPath.removeAll()
} catch {
self.isLoading = false
self.errorText = ErrorUtils.message(error: error)

Loading…
Cancel
Save