add structure building

multistore
Razmig Sarkissian 2 years ago
parent c89c212c11
commit 38d2f7d005
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 69
      PadelClub/Data/Tournament.swift
  3. 13
      PadelClub/Manager/PadelRule.swift
  4. 90
      PadelClub/Views/Components/StepperView.swift
  5. 16
      PadelClub/Views/Shared/MatchFormatPickerView.swift
  6. 1
      PadelClub/Views/Tournament/Screen/Screen.swift
  7. 374
      PadelClub/Views/Tournament/Screen/TableStructureView.swift
  8. 4
      PadelClub/Views/Tournament/Screen/TournamentSettingsView.swift
  9. 23
      PadelClub/Views/Tournament/TournamentInitView.swift
  10. 2
      PadelClub/Views/Tournament/TournamentView.swift

@ -80,6 +80,7 @@
FF8F264D2BAE0B4100650388 /* TournamentDatePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */; };
FF8F264F2BAE0B9600650388 /* MatchTypeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */; };
FF8F26512BAE0BAD00650388 /* MatchFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */; };
FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF8F26532BAE1E4400650388 /* TableStructureView.swift */; };
FFC1E1042BAC28C6008D6F59 /* ClubSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */; };
FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */; };
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */; };
@ -218,6 +219,7 @@
FF8F264A2BAE0B4100650388 /* TournamentDatePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentDatePickerView.swift; sourceTree = "<group>"; };
FF8F264E2BAE0B9600650388 /* MatchTypeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchTypeSelectionView.swift; sourceTree = "<group>"; };
FF8F26502BAE0BAD00650388 /* MatchFormatPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchFormatPickerView.swift; sourceTree = "<group>"; };
FF8F26532BAE1E4400650388 /* TableStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableStructureView.swift; sourceTree = "<group>"; };
FFC1E1032BAC28C6008D6F59 /* ClubSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubSearchView.swift; sourceTree = "<group>"; };
FFC1E1072BAC29FC008D6F59 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
FFC1E1092BAC2A77008D6F59 /* NetworkFederalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkFederalService.swift; sourceTree = "<group>"; };
@ -469,6 +471,7 @@
FF6EC8FF2B94794700EA7F5A /* PresentationContext.swift */,
FF70916D2B9108C600AB08DA /* InscriptionManagerView.swift */,
FF8F26422BADFE5B00650388 /* TournamentSettingsView.swift */,
FF8F26532BAE1E4400650388 /* TableStructureView.swift */,
FF8F26522BAE0E4E00650388 /* Components */,
);
path = Screen;
@ -809,6 +812,7 @@
C4A47D7D2B73CDC300ADC637 /* ClubV1.swift in Sources */,
FF8F263D2BAD627A00650388 /* TournamentConfiguratorView.swift in Sources */,
FFC1E10C2BAC7FB0008D6F59 /* ClubImportView.swift in Sources */,
FF8F26542BAE1E4400650388 /* TableStructureView.swift in Sources */,
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */,
FF3F74FF2B91A2D4004CFE0E /* AgendaDestination.swift in Sources */,
FF3795622B9396D0004EA093 /* PadelClubApp.xcdatamodeld in Sources */,

@ -9,21 +9,7 @@ import Foundation
import LeStorage
@Observable
class Tournament : ModelObject, Storable, Hashable {
static func == (lhs: Tournament, rhs: Tournament) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(event)
hasher.combine(creator)
hasher.combine(courtCount)
}
@ObservationIgnored
var undoManager: Int = 0
class Tournament : ModelObject, Storable {
static func resourceName() -> String { "tournaments" }
var id: String = Store.randomId()
@ -56,6 +42,12 @@ class Tournament : ModelObject, Storable, Hashable {
var teamsPerGroupStage: Int
var entryFee: Double?
@ObservationIgnored
var navigationPath: [Screen] = []
@ObservationIgnored
var undoManager: Int = 0
internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil) {
self.event = event
self.creator = creator
@ -87,9 +79,6 @@ class Tournament : ModelObject, Storable, Hashable {
self.entryFee = entryFee
}
@ObservationIgnored
var navigationPath: [Screen] = []
var rounds: Int {
4
}
@ -110,6 +99,12 @@ class Tournament : ModelObject, Storable, Hashable {
func settingsDescriptionLocalizedLabel() -> String {
[dayDuration.formatted() + " jour\(dayDuration.pluralSuffix)", courtCount.formatted() + " terrain\(courtCount.pluralSuffix)"].joined(separator: ", ")
}
func structureDescriptionLocalizedLabel() -> String {
let groupStageLabel: String? = groupStageCount > 0 ? groupStageCount.formatted() + " poule\(groupStageCount.pluralSuffix)" : nil
return [teamCount.formatted() + " équipes", groupStageLabel].compactMap({ $0 }).joined(separator: ", ")
}
}
extension Tournament {
@ -269,3 +264,41 @@ extension Tournament {
case initial
}
}
extension Tournament: Hashable {
static func == (lhs: Tournament, rhs: Tournament) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(event)
hasher.combine(creator)
hasher.combine(name)
hasher.combine(startDate)
hasher.combine(endDate)
hasher.combine(creationDate)
hasher.combine(isPrivate)
hasher.combine(groupStageFormat)
hasher.combine(roundFormat)
hasher.combine(loserRoundFormat)
hasher.combine(groupStageSortMode)
hasher.combine(groupStageCount)
hasher.combine(rankSourceDate)
hasher.combine(dayDuration)
hasher.combine(teamCount)
hasher.combine(teamSorting)
hasher.combine(federalCategory)
hasher.combine(federalLevelCategory)
hasher.combine(federalAgeCategory)
hasher.combine(groupStageCourtCount)
hasher.combine(seedCount)
hasher.combine(closedRegistrationDate)
hasher.combine(groupStageAdditionalQualified)
hasher.combine(courtCount)
hasher.combine(prioritizeClubMembers)
hasher.combine(qualifiedPerGroupStage)
hasher.combine(teamsPerGroupStage)
hasher.combine(entryFee)
}
}

@ -1029,6 +1029,19 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
}
}
func formattedEstimatedDuration() -> String {
Duration.seconds(estimatedDuration * 60).formatted(.units(allowed: [.minutes]))
}
func formattedEstimatedBreakDuration() -> String {
var label = Duration.seconds(breakTime.breakTime * 60).formatted(.units(allowed: [.minutes]))
if breakTime.matchCount > 1 {
label += " après \(breakTime.matchCount) match"
label += breakTime.matchCount.pluralSuffix
}
return label
}
var defaultEstimatedDuration: Int {
switch self {
case .twoSets:

@ -11,54 +11,80 @@ import SwiftUI
struct StepperView: View {
var title: String? = nil
@Binding var count: Int
var minimum: Int? = nil
var maximum: Int? = nil
var body: some View {
HStack {
Button(action: {
self._add()
}, label: {
Image(systemName: "plus.circle")
})
Button(action: {
self._subtract()
}, label: {
Image(systemName: "minus.circle")
})
VStack(spacing: 0) {
HStack(spacing: 16) {
Button(action: {
self._subtract()
}, label: {
Image(systemName: "minus.circle")
.resizable()
.scaledToFit()
.frame(width: 24)
})
.disabled(_minusIsDisabled())
.buttonStyle(.borderless)
}.padding(4.0)
TextField("00", value: $count, format: .number)
.keyboardType(.numberPad)
.fixedSize()
.font(.title2)
.monospacedDigit()
.onSubmit {
if let minimum, count < minimum {
count = minimum
} else if let maximum, count > maximum {
count = maximum
}
}
Button(action: {
self._add()
}, label: {
Image(systemName: "plus.circle")
.resizable()
.scaledToFit()
.frame(width: 24)
})
.disabled(_plusIsDisabled())
.buttonStyle(.borderless)
}
if let title {
Text(title + count.pluralSuffix).font(.caption)
}
}
.multilineTextAlignment(.trailing)
}
fileprivate func _minusIsDisabled() -> Bool {
count <= (minimum ?? 0)
}
fileprivate func _plusIsDisabled() -> Bool {
count >= (maximum ?? Int.max)
}
fileprivate func _add() {
if let maximum, self.count + 1 > maximum {
return
}
self.count += 1
}
fileprivate func _subtract() {
self.count -= 1
if let minimum, self.count < minimum {
self.count = minimum
if let minimum, self.count - 1 < minimum {
return
}
self.count -= 1
}
}
struct StepperTestView: View {
@State var quantity: Int = 5
var body: some View {
Text(quantity.formatted())
StepperView(count: self.$quantity, minimum: 0)
}
}
#Preview {
StepperTestView()
}
#Preview {
StepperView(count: .constant(1))
StepperView(title: "poule", count: .constant(1))
}

@ -37,16 +37,20 @@ struct MatchFormatPickerView: View {
Text("Durée").font(.caption)
}
HStack {
Text(matchFormat.format)
Text(matchFormat.format).font(.largeTitle)
Spacer()
VStack(alignment: .trailing) {
Text("~" + matchFormat.estimatedDuration.formatted() + " minutes")
Text(matchFormat.breakTime.breakTime.formatted() + " minutes de pause").foregroundStyle(.secondary)
if matchFormat.breakTime.matchCount > 1 {
Text("après \(matchFormat.breakTime.matchCount) match" + matchFormat.breakTime.matchCount.pluralSuffix).foregroundStyle(.secondary)
}
Text("~" + matchFormat.formattedEstimatedDuration())
Text(matchFormat.formattedEstimatedBreakDuration() + " de pause").foregroundStyle(.secondary).font(.subheadline)
}
}
}
}
}
#Preview {
List {
MatchFormatPickerView(headerLabel: "Test", matchFormat: .constant(MatchFormat.superTie))
}
}

@ -11,4 +11,5 @@ enum Screen: String, Codable {
case inscription
case groupStage
case settings
case structure
}

@ -0,0 +1,374 @@
//
// TableStructureView.swift
// Padel Tournament
//
// Created by Razmig Sarkissian on 04/10/2023.
//
import SwiftUI
struct TableStructureView: View {
@Environment(Tournament.self) private var tournament: Tournament
@EnvironmentObject private var dataStore: DataStore
@Environment(\.dismiss) var dismiss
@State private var presentRefreshStructureWarning: Bool = false
@State private var teamCount: Int = 0
@State private var groupStageCount: Int = 0
@State private var teamsPerGroupStage: Int = 0
@State private var qualifiedPerGroupStage: Int = 0
@State private var groupStageAdditionalQualified: Int = 0
@State private var updatedElements: Set<StructureElement> = Set()
@FocusState private var stepperFieldIsFocused: Bool
var qualifiedFromGroupStage: Int {
groupStageCount * qualifiedPerGroupStage
}
var teamsFromGroupStages: Int {
groupStageCount * teamsPerGroupStage
}
var maxMoreQualified: Int {
if teamsPerGroupStage - qualifiedPerGroupStage > 1 {
return groupStageCount
} else if teamsPerGroupStage - qualifiedPerGroupStage == 1 {
return groupStageCount - 1
} else {
return 0
}
}
var moreQualifiedLabel: String {
if groupStageAdditionalQualified == 0 { return "Aucun" }
return (groupStageAdditionalQualified > 1 ? "les \(groupStageAdditionalQualified)" : "le") + " meilleur\(groupStageAdditionalQualified.pluralSuffix) " + (qualifiedPerGroupStage + 1).ordinalFormatted()
}
var maxGroupStages: Int {
teamCount / max(1, teamsPerGroupStage)
}
@ViewBuilder
var body: some View {
List {
Section {
LabeledContent {
StepperView(count: $teamCount, minimum: 4, maximum: 128)
} label: {
Text("Nombre d'équipes")
}
LabeledContent {
StepperView(count: $groupStageCount, minimum: 0, maximum: maxGroupStages)
} label: {
Text("Nombre de poules")
}
}
if groupStageCount > 0 {
if (teamCount / groupStageCount) > 1 {
Section {
LabeledContent {
StepperView(count: $teamsPerGroupStage, minimum: 2, maximum: (teamCount / groupStageCount))
} label: {
Text("Équipes par poule")
}
LabeledContent {
StepperView(count: $qualifiedPerGroupStage, minimum: 1, maximum: (teamsPerGroupStage-1))
} label: {
Text("Qualifiés par poule")
}
if qualifiedPerGroupStage < teamsPerGroupStage - 1 {
LabeledContent {
StepperView(count: $groupStageAdditionalQualified, minimum: 0, maximum: maxMoreQualified)
} label: {
Text("Qualifiés supplémentaires").foregroundStyle(.secondary).font(.caption)
Text(moreQualifiedLabel)
}
.onChange(of: groupStageAdditionalQualified) {
if groupStageAdditionalQualified == groupStageCount {
qualifiedPerGroupStage += 1
groupStageAdditionalQualified -= groupStageCount
}
}
}
if groupStageCount > 0 && teamsPerGroupStage > 0 {
LabeledContent {
let mp = teamsPerGroupStage * (teamsPerGroupStage - 1) / 2
Text(mp.formatted())
} label: {
Text("Matchs à jouer par poule")
}
}
}
} else {
ContentUnavailableView("Erreur", systemImage: "divide.circle.fill", description: Text("Il y n'y a pas assez d'équipe pour ce nombre de poule."))
}
}
Section {
let tf = max(teamCount - teamsFromGroupStages + qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0), 0)
if groupStageCount > 0 {
LabeledContent {
Text(teamsFromGroupStages.formatted())
} label: {
Text("Équipes en poule")
}
LabeledContent {
Text((qualifiedFromGroupStage + (groupStageCount > 0 ? groupStageAdditionalQualified : 0)).formatted())
} label: {
Text("Équipes qualifiées de poule")
}
}
LabeledContent {
let tsPure = max(teamCount - groupStageCount * teamsPerGroupStage, 0)
Text(tsPure.formatted())
} label: {
Text("Nombre de têtes de série")
}
LabeledContent {
Text(tf.formatted())
} label: {
Text("Équipes en tableau final")
}
}
}
.focused($stepperFieldIsFocused)
.onChange(of: stepperFieldIsFocused) {
if stepperFieldIsFocused {
DispatchQueue.main.async {
UIApplication.shared.sendAction(#selector(UIResponder.selectAll(_:)), to: nil, from: nil, for: nil)
}
}
}
.toolbarBackground(.visible, for: .navigationBar)
.onAppear {
teamCount = tournament.teamCount
groupStageCount = tournament.groupStageCount
teamsPerGroupStage = tournament.teamsPerGroupStage
qualifiedPerGroupStage = tournament.qualifiedPerGroupStage
groupStageAdditionalQualified = tournament.groupStageAdditionalQualified
}
.onChange(of: teamCount) {
if teamCount != tournament.teamCount {
updatedElements.insert(.teamCount)
} else {
updatedElements.remove(.teamCount)
}
}
.onChange(of: groupStageCount) {
if groupStageCount != tournament.groupStageCount {
updatedElements.insert(.groupStageCount)
} else {
updatedElements.remove(.groupStageCount)
}
}
.onChange(of: teamsPerGroupStage) {
if teamsPerGroupStage != tournament.teamsPerGroupStage {
updatedElements.insert(.teamsPerGroupStage)
} else {
updatedElements.remove(.teamsPerGroupStage)
} }
.onChange(of: qualifiedPerGroupStage) {
if qualifiedPerGroupStage != tournament.qualifiedPerGroupStage {
updatedElements.insert(.qualifiedPerGroupStage)
} else {
updatedElements.remove(.qualifiedPerGroupStage)
} }
.onChange(of: groupStageAdditionalQualified) {
if groupStageAdditionalQualified != tournament.groupStageAdditionalQualified {
updatedElements.insert(.groupStageAdditionalQualified)
} else {
updatedElements.remove(.groupStageAdditionalQualified)
} }
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Confirmer") {
stepperFieldIsFocused = false
_verifyValueIntegrity()
}
}
ToolbarItem(placement: .confirmationAction) {
if tournament.state() == .initial {
Button("Valider") {
_save(rebuildEverything: true)
dismiss()
}
.clipShape(Capsule())
.buttonStyle(.bordered)
.disabled(updatedElements.isEmpty)
} else {
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding })
Button("Valider", role: .destructive) {
if requirements.isEmpty {
_save(rebuildEverything: false)
dismiss()
} else {
presentRefreshStructureWarning = true
}
}
.clipShape(Capsule())
.buttonStyle(.bordered)
.disabled(updatedElements.isEmpty)
.confirmationDialog("Mise à jour de la structure", isPresented: $presentRefreshStructureWarning, actions: {
if requirements.allSatisfy({ $0 == .groupStage }) {
Button("Mettre à jour les poules") {
_save(rebuildEverything: false)
dismiss()
}
}
Button("Tout mettre à jour", role: .destructive) {
_save(rebuildEverything: true)
dismiss()
}
}, message: {
ForEach(Array(requirements)) { requirement in
Text(requirement.rebuildingRequirementMessage)
}
})
}
}
}
.navigationTitle("Structure")
.navigationBarTitleDisplayMode(.inline)
}
private func _save(rebuildEverything: Bool = false) {
do {
let requirements = Set(updatedElements.compactMap { $0.requiresRebuilding })
if (rebuildEverything == false && requirements.contains(.all)) || rebuildEverything {
// if let matches = tournament.matchs {
// tournament.removeFromMatchs(matches)
// }
// tournament.additionalRounds = 0
// tournament.orderedEntries.forEach { entrant in
// entrant.initialPosition = 0
// }
// tournament.hiddenRounds = nil
}
tournament.teamCount = teamCount
tournament.groupStageCount = groupStageCount
tournament.teamsPerGroupStage = teamsPerGroupStage
tournament.qualifiedPerGroupStage = qualifiedPerGroupStage
tournament.groupStageAdditionalQualified = groupStageAdditionalQualified
if (rebuildEverything == false && requirements.contains(.all)) || rebuildEverything {
// tournament.build()
} else if (rebuildEverything == false && requirements.contains(.groupStage)) {
// tournament.buildGroupStages()
}
try dataStore.tournaments.addOrUpdate(instance: tournament)
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
private func _verifyValueIntegrity() {
if teamCount > 128 {
teamCount = 128
}
if groupStageCount > maxGroupStages {
groupStageCount = maxGroupStages
}
if teamCount < 4 {
teamCount = 4
}
if groupStageCount < 0 {
groupStageCount = 0
}
if groupStageCount > 0 {
if teamsPerGroupStage > (teamCount / groupStageCount) {
teamsPerGroupStage = (teamCount / groupStageCount)
}
if qualifiedPerGroupStage > (teamsPerGroupStage-1) {
qualifiedPerGroupStage = (teamsPerGroupStage-1)
}
if groupStageAdditionalQualified > maxMoreQualified {
groupStageAdditionalQualified = maxMoreQualified
}
if teamsPerGroupStage < 2 {
teamsPerGroupStage = 2
}
if qualifiedPerGroupStage < 1 {
qualifiedPerGroupStage = 1
}
if groupStageAdditionalQualified < 0 {
groupStageAdditionalQualified = 0
}
}
}
}
extension TableStructureView {
enum StructureElement: Int, Identifiable {
case teamCount
case groupStageCount
case teamsPerGroupStage
case qualifiedPerGroupStage
case groupStageAdditionalQualified
var id: Int { self.rawValue }
var requiresRebuilding: RebuildingRequirement? {
switch self {
case .teamCount:
return .all
case .groupStageCount:
return .groupStage
case .teamsPerGroupStage:
return .groupStage
case .qualifiedPerGroupStage:
return nil
case .groupStageAdditionalQualified:
return nil
}
}
}
enum RebuildingRequirement: Int, Identifiable {
case groupStage
case all
var id: Int { self.rawValue }
var rebuildingRequirementMessage: String {
switch self {
case .groupStage:
return "Si vous le souhaitez, seulement les poules seront mis à jour. Le tableau ne sera pas modifié."
case .all:
return "Tous les matchs seront re-générés. La position des têtes de série sera remise à zéro et les poules seront reconstruites."
}
}
}
}
#Preview {
NavigationStack {
TableStructureView()
.environment(Tournament.mock())
.environmentObject(DataStore.shared)
}
}

@ -42,13 +42,11 @@ struct TournamentSettingsView: View {
Text("Nom du tournoi")
}
TournamentLevelPickerView()
TournamentDurationManagerView()
TournamentFieldsManagerView()
TournamentDatePickerView()
TournamentFormatSelectionView()
TournamentLevelPickerView()
}
.navigationTitle("Réglages")
.toolbarBackground(.visible, for: .navigationBar)

@ -23,17 +23,18 @@ struct TournamentInitView: View {
} footer: {
Text("La date, la catégorie, le niveau, le nombre de terrain, les formats, etc.")
}
//
// Section {
// NavigationLink {
// TableStructureView(tournament: tournament)
// } label: {
// Label("Structure", systemImage: "hammer")
// .badge(tournament.structureDescriptionLocalizedLabel)
// }
// } footer: {
// Text("Nombre d'équipes, de poules, de qualifiés sortant, etc.")
// }
Section {
NavigationLink(value: Screen.structure) {
LabeledContent {
Text(tournament.structureDescriptionLocalizedLabel())
} label: {
Label("Structure", systemImage: "hammer")
}
}
} footer: {
Text("Nombre d'équipes, de poules, de qualifiés sortant, etc.")
}
}
}

@ -27,6 +27,8 @@ struct TournamentView: View {
.navigationDestination(for: Screen.self, destination: { screen in
Group {
switch screen {
case .structure:
TableStructureView()
case .settings:
TournamentSettingsView()
case .inscription:

Loading…
Cancel
Save