cleanup generation and add tournament sharing

sync2
Laurent 11 months ago
parent 62fd9c5610
commit 9ebdffddd7
  1. 16
      PadelClub.xcodeproj/project.pbxproj
  2. 3
      PadelClub/Data/Gen/BaseClub.swift
  3. 1
      PadelClub/Data/Gen/BaseCourt.swift
  4. 10
      PadelClub/Data/Gen/BaseCustomUser.swift
  5. 1
      PadelClub/Data/Gen/BaseDateInterval.swift
  6. 1
      PadelClub/Data/Gen/BaseEvent.swift
  7. 1
      PadelClub/Data/Gen/BaseGroupStage.swift
  8. 1
      PadelClub/Data/Gen/BaseMatch.swift
  9. 1
      PadelClub/Data/Gen/BaseMatchScheduler.swift
  10. 1
      PadelClub/Data/Gen/BaseMonthData.swift
  11. 1
      PadelClub/Data/Gen/BasePlayerRegistration.swift
  12. 1
      PadelClub/Data/Gen/BasePurchase.swift
  13. 1
      PadelClub/Data/Gen/BaseRound.swift
  14. 1
      PadelClub/Data/Gen/BaseTeamRegistration.swift
  15. 1
      PadelClub/Data/Gen/BaseTeamScore.swift
  16. 1
      PadelClub/Data/Gen/BaseTournament.swift
  17. 3
      PadelClub/Data/Gen/Court.json
  18. 6
      PadelClub/Data/Gen/CustomUser.json
  19. 3
      PadelClub/Data/Gen/DateInterval.json
  20. 3
      PadelClub/Data/Gen/Event.json
  21. 3
      PadelClub/Data/Gen/GroupStage.json
  22. 1
      PadelClub/Data/Gen/Match.json
  23. 1
      PadelClub/Data/Gen/MatchScheduler.json
  24. 1
      PadelClub/Data/Gen/MonthData.json
  25. 1
      PadelClub/Data/Gen/PlayerRegistration.json
  26. 1
      PadelClub/Data/Gen/Purchase.json
  27. 1
      PadelClub/Data/Gen/Round.json
  28. 1
      PadelClub/Data/Gen/TeamRegistration.json
  29. 1
      PadelClub/Data/Gen/TeamScore.json
  30. 1
      PadelClub/Data/Gen/Tournament.json
  31. 6
      PadelClub/Data/Gen/generator.py
  32. 8
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  33. 8
      PadelClub/Views/Tournament/TournamentView.swift
  34. 113
      PadelClub/Views/User/ShareModelView.swift
  35. 168
      PadelClub/Views/User/UserSearchView.swift

@ -17,9 +17,9 @@
C425D4122B6D249E002A7B48 /* PadelClubTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4112B6D249E002A7B48 /* PadelClubTests.swift */; };
C425D41C2B6D249E002A7B48 /* PadelClubUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D41B2B6D249E002A7B48 /* PadelClubUITests.swift */; };
C425D41E2B6D249E002A7B48 /* PadelClubUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D41D2B6D249E002A7B48 /* PadelClubUITestsLaunchTests.swift */; };
C4339BFB2CFF7D68004E5F09 /* UserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4339BFA2CFF7D64004E5F09 /* UserSearchView.swift */; };
C4339BFC2CFF7D68004E5F09 /* UserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4339BFA2CFF7D64004E5F09 /* UserSearchView.swift */; };
C4339BFD2CFF7D68004E5F09 /* UserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4339BFA2CFF7D64004E5F09 /* UserSearchView.swift */; };
C4339BFB2CFF7D68004E5F09 /* ShareModelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4339BFA2CFF7D64004E5F09 /* ShareModelView.swift */; };
C4339BFC2CFF7D68004E5F09 /* ShareModelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4339BFA2CFF7D64004E5F09 /* ShareModelView.swift */; };
C4339BFD2CFF7D68004E5F09 /* ShareModelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4339BFA2CFF7D64004E5F09 /* ShareModelView.swift */; };
C4489BE22C05BF5000043F3D /* DebugSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4489BE12C05BF5000043F3D /* DebugSettingsView.swift */; };
C44B79112BBDA63A00906534 /* Locale+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44B79102BBDA63A00906534 /* Locale+Extensions.swift */; };
C45BAE3B2BC6DF10002EEC8A /* SyncedProducts.storekit in Resources */ = {isa = PBXBuildFile; fileRef = C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */; };
@ -975,7 +975,7 @@
C425D4172B6D249E002A7B48 /* PadelClubUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PadelClubUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C425D41B2B6D249E002A7B48 /* PadelClubUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubUITests.swift; sourceTree = "<group>"; };
C425D41D2B6D249E002A7B48 /* PadelClubUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PadelClubUITestsLaunchTests.swift; sourceTree = "<group>"; };
C4339BFA2CFF7D64004E5F09 /* UserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearchView.swift; sourceTree = "<group>"; };
C4339BFA2CFF7D64004E5F09 /* ShareModelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareModelView.swift; sourceTree = "<group>"; };
C4489BE12C05BF5000043F3D /* DebugSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugSettingsView.swift; sourceTree = "<group>"; };
C44B79102BBDA63A00906534 /* Locale+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Locale+Extensions.swift"; sourceTree = "<group>"; };
C45BAE3A2BC6DF10002EEC8A /* SyncedProducts.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = SyncedProducts.storekit; sourceTree = "<group>"; };
@ -1547,7 +1547,7 @@
C4A47D852B7BA33F00ADC637 /* User */ = {
isa = PBXGroup;
children = (
C4339BFA2CFF7D64004E5F09 /* UserSearchView.swift */,
C4339BFA2CFF7D64004E5F09 /* ShareModelView.swift */,
C4A47DB22B86387500ADC637 /* AccountView.swift */,
C4A47DA82B85F82100ADC637 /* ChangePasswordView.swift */,
C4A47DA52B83948E00ADC637 /* LoginView.swift */,
@ -2457,7 +2457,7 @@
C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */,
FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */,
FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */,
C4339BFB2CFF7D68004E5F09 /* UserSearchView.swift in Sources */,
C4339BFB2CFF7D68004E5F09 /* ShareModelView.swift in Sources */,
FF8F26412BADFC8700650388 /* TournamentInitView.swift in Sources */,
C4A47D8A2B7BBB6500ADC637 /* SubscriptionView.swift in Sources */,
FFD655D82C8DE27400E5B35E /* TournamentLookUpView.swift in Sources */,
@ -2836,7 +2836,7 @@
FF4CBF992C996C0600151637 /* StoreManager.swift in Sources */,
FF4CBF9A2C996C0600151637 /* SearchViewModel.swift in Sources */,
FF4CBF9B2C996C0600151637 /* PlayerRegistration.swift in Sources */,
C4339BFD2CFF7D68004E5F09 /* UserSearchView.swift in Sources */,
C4339BFD2CFF7D68004E5F09 /* ShareModelView.swift in Sources */,
FF4CBF9C2C996C0600151637 /* ImportedPlayerView.swift in Sources */,
FF4CBF9D2C996C0600151637 /* EditingTeamView.swift in Sources */,
FF4CBF9E2C996C0600151637 /* NetworkManagerError.swift in Sources */,
@ -3103,7 +3103,7 @@
FF70FB182C90584900129CC2 /* StoreManager.swift in Sources */,
FF70FB192C90584900129CC2 /* SearchViewModel.swift in Sources */,
FF70FB1A2C90584900129CC2 /* PlayerRegistration.swift in Sources */,
C4339BFC2CFF7D68004E5F09 /* UserSearchView.swift in Sources */,
C4339BFC2CFF7D68004E5F09 /* ShareModelView.swift in Sources */,
FF70FB1B2C90584900129CC2 /* ImportedPlayerView.swift in Sources */,
FF70FB1C2C90584900129CC2 /* EditingTeamView.swift in Sources */,
FF70FB1D2C90584900129CC2 /* NetworkManagerError.swift in Sources */,

@ -10,7 +10,6 @@ class BaseClub: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "clubs" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false }
var id: String = Store.randomId()
var creator: String? = nil
@ -137,4 +136,4 @@ class BaseClub: SyncedModelObject, SyncedStorable {
]
}
}
}

@ -10,7 +10,6 @@ class BaseCourt: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "courts" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false }
var id: String = Store.randomId()
var index: Int = 0

@ -10,7 +10,6 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "users" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [.post] }
static func filterByStoreIdentifier() -> Bool { return false }
var id: String = Store.randomId()
var username: String = ""
@ -34,6 +33,7 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable {
var loserBracketMatchFormatPreference: MatchFormat? = nil
var loserBracketMode: LoserBracketMode = .automatic
var deviceId: String? = nil
var agents: [String] = []
init(
id: String = Store.randomId(),
@ -57,7 +57,8 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable {
groupStageMatchFormatPreference: MatchFormat? = nil,
loserBracketMatchFormatPreference: MatchFormat? = nil,
loserBracketMode: LoserBracketMode = .automatic,
deviceId: String? = nil
deviceId: String? = nil,
agents: [String] = []
) {
super.init()
self.id = id
@ -82,6 +83,7 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable {
self.loserBracketMatchFormatPreference = loserBracketMatchFormatPreference
self.loserBracketMode = loserBracketMode
self.deviceId = deviceId
self.agents = agents
}
enum CodingKeys: String, CodingKey {
@ -107,6 +109,7 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable {
case _loserBracketMatchFormatPreference = "loserBracketMatchFormatPreference"
case _loserBracketMode = "loserBracketMode"
case _deviceId = "deviceId"
case _agents = "agents"
}
required init(from decoder: Decoder) throws {
@ -133,6 +136,7 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable {
self.loserBracketMatchFormatPreference = try container.decodeIfPresent(MatchFormat.self, forKey: ._loserBracketMatchFormatPreference) ?? nil
self.loserBracketMode = try container.decodeIfPresent(LoserBracketMode.self, forKey: ._loserBracketMode) ?? .automatic
self.deviceId = try container.decodeIfPresent(String.self, forKey: ._deviceId) ?? nil
self.agents = try container.decodeIfPresent([String].self, forKey: ._agents) ?? []
try super.init(from: decoder)
}
@ -160,6 +164,7 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable {
try container.encode(self.loserBracketMatchFormatPreference, forKey: ._loserBracketMatchFormatPreference)
try container.encode(self.loserBracketMode, forKey: ._loserBracketMode)
try container.encode(self.deviceId, forKey: ._deviceId)
try container.encode(self.agents, forKey: ._agents)
try super.encode(to: encoder)
}
@ -187,6 +192,7 @@ class BaseCustomUser: SyncedModelObject, SyncedStorable {
self.loserBracketMatchFormatPreference = customuser.loserBracketMatchFormatPreference
self.loserBracketMode = customuser.loserBracketMode
self.deviceId = customuser.deviceId
self.agents = customuser.agents
}
static func relationships() -> [Relationship] {

@ -10,7 +10,6 @@ class BaseDateInterval: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "date-intervals" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false }
var id: String = Store.randomId()
var event: String = ""

@ -10,7 +10,6 @@ class BaseEvent: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "events" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false }
var id: String = Store.randomId()
var creator: String? = nil

@ -10,7 +10,6 @@ class BaseGroupStage: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "group-stages" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true }
var id: String = Store.randomId()
var tournament: String = ""

@ -10,7 +10,6 @@ class BaseMatch: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "matches" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true }
var id: String = Store.randomId()
var round: String? = nil

@ -10,7 +10,6 @@ class BaseMatchScheduler: BaseModelObject, Storable {
static func resourceName() -> String { return "match-scheduler" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false }
var id: String = Store.randomId()
var tournament: String = ""

@ -10,7 +10,6 @@ class BaseMonthData: BaseModelObject, Storable {
static func resourceName() -> String { return "month-data" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false }
var id: String = Store.randomId()
var monthKey: String = ""

@ -10,7 +10,6 @@ class BasePlayerRegistration: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "player-registrations" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true }
var id: String = Store.randomId()
var teamRegistration: String? = nil

@ -8,7 +8,6 @@ class BasePurchase: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "purchases" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false }
var id: UInt64 = 0
var user: String = ""

@ -10,7 +10,6 @@ class BaseRound: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "rounds" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true }
var id: String = Store.randomId()
var tournament: String = ""

@ -10,7 +10,6 @@ class BaseTeamRegistration: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "team-registrations" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true }
var id: String = Store.randomId()
var tournament: String = ""

@ -10,7 +10,6 @@ class BaseTeamScore: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "team-scores" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return true }
var id: String = Store.randomId()
var match: String = ""

@ -10,7 +10,6 @@ class BaseTournament: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "tournaments" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false }
var id: String = Store.randomId()
var event: String? = nil

@ -36,8 +36,7 @@
"defaultValue": "false"
}
],
"tokenExemptedMethods": [],
"filterByStoreIdentifier": false
"tokenExemptedMethods": []
}
]
}

@ -6,7 +6,6 @@
"synchronizable": true,
"observable": true,
"tokenExemptedMethods": ["post"],
"filterByStoreIdentifier": false,
"properties": [
{
"name": "id",
@ -125,6 +124,11 @@
"type": "String",
"optional": true,
"defaultValue": "nil"
},
{
"name": "agents",
"type": "[String]",
"defaultValue": "[]"
}
]
}

@ -27,8 +27,7 @@
"type": "Date"
}
],
"tokenExemptedMethods": [],
"filterByStoreIdentifier": false
"tokenExemptedMethods": []
}
]
}

@ -42,8 +42,7 @@
"defaultValue": "nil"
}
],
"tokenExemptedMethods": [],
"filterByStoreIdentifier": false
"tokenExemptedMethods": []
}
]
}

@ -47,8 +47,7 @@
"defaultValue": "0"
}
],
"tokenExemptedMethods": [],
"filterByStoreIdentifier": true
"tokenExemptedMethods": []
}
]
}

@ -5,7 +5,6 @@
"synchronizable": true,
"observable": true,
"tokenExemptedMethods": [],
"filterByStoreIdentifier": true,
"properties": [
{
"name": "id",

@ -6,7 +6,6 @@
"synchronizable": false,
"observable": true,
"tokenExemptedMethods": [],
"filterByStoreIdentifier": false,
"properties": [
{
"name": "id",

@ -6,7 +6,6 @@
"synchronizable": false,
"observable": true,
"tokenExemptedMethods": [],
"filterByStoreIdentifier": false,
"properties": [
{
"name": "id",

@ -4,7 +4,6 @@
"name": "PlayerRegistration",
"synchronizable": true,
"sideStorable": true,
"filterByStoreIdentifier": true,
"observable": true,
"relationshipNames": ["teamRegistration"],
"properties": [

@ -45,7 +45,6 @@
}
],
"tokenExemptedMethods": [],
"filterByStoreIdentifier": false,
"relationshipNames": []
}
]

@ -4,7 +4,6 @@
"name": "Round",
"synchronizable": true,
"sideStorable": true,
"filterByStoreIdentifier": true,
"observable": true,
"relationshipNames": [],
"properties": [

@ -4,7 +4,6 @@
"name": "TeamRegistration",
"synchronizable": true,
"sideStorable": true,
"filterByStoreIdentifier": true,
"observable": true,
"relationshipNames": [],
"properties": [

@ -4,7 +4,6 @@
"name": "TeamScore",
"synchronizable": true,
"sideStorable": true,
"filterByStoreIdentifier": true,
"observable": true,
"relationshipNames": ["match"],
"properties": [

@ -4,7 +4,6 @@
"name": "Tournament",
"synchronizable": true,
"copyable": true,
"filterByStoreIdentifier": false,
"observable": true,
"relationshipNames": [],
"properties": [

@ -24,7 +24,6 @@ class SwiftModelGenerator:
resource = self.make_resource_name(model_name)
resource_name = model_data.get("resource_name", resource)
token_exempted = model_data.get("tokenExemptedMethods", [])
filter_by_store = model_data.get("filterByStoreIdentifier", False)
lines = ["// Generated by SwiftModelGenerator", "// Do not modify this file manually", ""]
@ -44,7 +43,7 @@ class SwiftModelGenerator:
lines.append("")
# Add SyncedStorable protocol requirements
lines.extend(self._generate_protocol_requirements(resource_name, token_exempted, filter_by_store))
lines.extend(self._generate_protocol_requirements(resource_name, token_exempted))
lines.append("")
# Properties
@ -347,7 +346,7 @@ class SwiftModelGenerator:
lines.append(" }")
return lines
def _generate_protocol_requirements(self, resource_name: str, token_exempted: List[str], filter_by_store: bool) -> List[str]:
def _generate_protocol_requirements(self, resource_name: str, token_exempted: List[str]) -> List[str]:
"""Generate the static functions required by SyncedStorable protocol."""
# Convert HTTP methods to proper format
formatted_methods = [f".{method.lower()}" for method in token_exempted]
@ -356,7 +355,6 @@ class SwiftModelGenerator:
return [
f" static func resourceName() -> String {{ return \"{resource_name}\" }}",
f" static func tokenExemptedMethods() -> [HTTPMethod] {{ return [{methods_str}] }}",
f" static func filterByStoreIdentifier() -> Bool {{ return {str(filter_by_store).lower()} }}"
]
def _generate_relationships(self, model_name, properties: List[Dict[str, Any]]) -> List[str]:

@ -121,13 +121,7 @@ struct EventListView: View {
NavigationLink(value: tournament) {
TournamentCellView(tournament: tournament)
.popover(isPresented: self.$showUserSearch) {
UserSearchView { user in
do {
try StoreCenter.main.giveUserAccess(user.id, data: tournament)
} catch {
Logger.error(error)
}
}
ShareModelView(instance: tournament)
}
}
.contextMenu {

@ -112,13 +112,7 @@ struct TournamentView: View {
case .print:
PrintSettingsView(tournament: tournament)
case .share:
UserSearchView { user in
do {
try StoreCenter.main.giveUserAccess(user.id, data: tournament)
} catch {
Logger.error(error)
}
}
ShareModelView(instance: tournament)
}
}
.environment(tournament)

@ -0,0 +1,113 @@
//
// UserSearchView.swift
// PadelClub
//
// Created by Laurent Morvillier on 03/12/2024.
//
import Combine
import LeStorage
import SwiftUI
class UserSearchViewModel: ObservableObject {
@Published var searchText = ""
@Published var userNames: [ShortUser] = []
@Published var users: [String] = []
@Published var availableUsers: [ShortUser] = []
@Published var selectedUsers: [String] = []
init() {
Task {
do {
let service = try StoreCenter.main.service()
let userNames = try await service.getUserNames()
DispatchQueue.main.async {
self.userNames = userNames
self.availableUsers = self.users.compactMap { userId in
self.userNames.first(where: { $0.id == userId })
}
}
} catch {
Logger.error(error)
}
}
}
func userTapped(_ user: String) {
if let index = self.selectedUsers.firstIndex(of: user) {
self.selectedUsers.remove(at: index)
} else {
self.selectedUsers.append(user)
}
}
func contains(_ user: String) -> Bool {
return self.selectedUsers.firstIndex(of: user) != nil
}
}
struct ShareModelView<T: SyncedStorable> : View {
@StateObject private var viewModel = UserSearchViewModel()
let instance: T
var body: some View {
NavigationView {
if !self.viewModel.availableUsers.isEmpty {
List {
ForEach(self.viewModel.availableUsers, id: \.id) { user in
let isSelected = viewModel.contains(user.id)
UserRow(user: user, isSelected: isSelected)
.contentShape(Rectangle())
.onTapGesture {
self.viewModel.userTapped(user.id)
self._modifyAuthorizedUsersList()
}
}
}
.listStyle(PlainListStyle())
.navigationTitle("Partage")
} else {
ContentUnavailableView("Si vous souhaitez partager votre tournoi avec d'autres utilisateurs, veuillez contacter notre support", image: "person.fill.xmark")
}
}.onAppear {
self.viewModel.selectedUsers = StoreCenter.main.authorizedUsers(for: self.instance.stringId)
self.viewModel.users = DataStore.shared.user.agents
}
}
fileprivate func _modifyAuthorizedUsersList() {
do {
try StoreCenter.main.setAuthorizedUsers(for: self.instance, users: self.viewModel.selectedUsers)
} catch {
Logger.error(error)
}
}
}
struct UserRow: View {
let user: ShortUser
let isSelected: Bool
var body: some View {
HStack {
Text("\(user.firstName) \(user.lastName)")
Spacer()
if self.isSelected {
Image(systemName: "checkmark").foregroundStyle(.logoOrange)
}
}
.padding(.vertical, 4)
}
}
// Preview provider
struct ShareModelView_Previews: PreviewProvider {
static var previews: some View {
ShareModelView(instance: Tournament.fake())
}
}

@ -1,168 +0,0 @@
//
// UserSearchView.swift
// PadelClub
//
// Created by Laurent Morvillier on 03/12/2024.
//
import Combine
import LeStorage
import SwiftUI
class UserSearchViewModel: ObservableObject {
@Published var searchText = ""
@Published var users: [ShortUser] = []
@Published var isLoading = false
@Published var error: String?
@Published var selectedUser: ShortUser? = nil
private var cancellables = Set<AnyCancellable>()
private var originalUsers: [ShortUser] = []
private var lastSearchTerm = ""
init() {
// Debounce search to avoid too many requests
$searchText
.removeDuplicates()
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.sink { [weak self] searchTerm in
self?.handleSearch(searchTerm)
}
.store(in: &cancellables)
}
private func handleSearch(_ searchTerm: String) {
guard !searchTerm.isEmpty else {
users = []
return
}
// If going backwards in search, filter existing results
if searchTerm.count < lastSearchTerm.count && !originalUsers.isEmpty {
filterExistingResults(searchTerm)
return
}
// Otherwise, make a new request
performServerSearch(searchTerm)
}
private func filterExistingResults(_ searchTerm: String) {
users = originalUsers.filter { user in
user.firstName.localizedCaseInsensitiveContains(searchTerm)
|| user.lastName.localizedCaseInsensitiveContains(searchTerm)
}
}
private func performServerSearch(_ searchTerm: String) {
isLoading = true
error = nil
Task {
do {
let services = try StoreCenter.main.service()
let searchResults = try await services.searchUsers(string: searchTerm)
await MainActor.run {
self.originalUsers = searchResults
self.users = searchResults
self.lastSearchTerm = searchTerm
self.isLoading = false
}
} catch {
await MainActor.run {
self.error = error.localizedDescription
self.isLoading = false
}
}
}
}
}
struct UserSearchView: View {
@StateObject private var viewModel = UserSearchViewModel()
var handler: (ShortUser) -> Void
var body: some View {
NavigationView {
VStack {
searchField
if viewModel.isLoading {
loadingView
} else if let error = viewModel.error {
errorView(error)
} else {
List {
ForEach(viewModel.users, id: \.id) { user in
let isSelected = (user.id == viewModel.selectedUser?.id)
UserRow(user: user, isSelected: isSelected)
.contentShape(Rectangle())
.onTapGesture {
viewModel.selectedUser = user
}
}
}
.listStyle(PlainListStyle())
}
}
.navigationTitle("Search Users")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Select") {
if let selectedUser = viewModel.selectedUser {
handler(selectedUser)
}
}
.disabled(viewModel.selectedUser == nil)
}
}
}
}
private var searchField: some View {
TextField("Search users...", text: $viewModel.searchText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
private var loadingView: some View {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.scaleEffect(1.5)
.frame(maxHeight: .infinity)
}
private func errorView(_ error: String) -> some View {
Text(error)
.foregroundColor(.red)
.frame(maxHeight: .infinity)
}
}
struct UserRow: View {
let user: ShortUser
let isSelected: Bool
var body: some View {
HStack {
Text("\(user.firstName) \(user.lastName)")
Spacer()
if self.isSelected {
Image(systemName: "checkmark").tint(.logoOrange)
}
}
.padding(.vertical, 4)
}
}
// Preview provider
struct UserSearchView_Previews: PreviewProvider {
static var previews: some View {
UserSearchView { user in
}
}
}
Loading…
Cancel
Save