Adds a way to share a tournament with others

sync2
Laurent 11 months ago
parent eb1f69ec97
commit 62fd9c5610
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 2
      PadelClub/AppDelegate.swift
  3. 1
      PadelClub/ViewModel/Screen.swift
  4. 6
      PadelClub/Views/Components/Labels.swift
  5. 17
      PadelClub/Views/Navigation/Agenda/EventListView.swift
  6. 12
      PadelClub/Views/Tournament/TournamentView.swift
  7. 168
      PadelClub/Views/User/UserSearchView.swift

@ -17,6 +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 */; };
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 */; };
@ -972,6 +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>"; };
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>"; };
@ -1543,6 +1547,7 @@
C4A47D852B7BA33F00ADC637 /* User */ = {
isa = PBXGroup;
children = (
C4339BFA2CFF7D64004E5F09 /* UserSearchView.swift */,
C4A47DB22B86387500ADC637 /* AccountView.swift */,
C4A47DA82B85F82100ADC637 /* ChangePasswordView.swift */,
C4A47DA52B83948E00ADC637 /* LoginView.swift */,
@ -2452,6 +2457,7 @@
C4A47D9F2B7D0BCE00ADC637 /* StepperView.swift in Sources */,
FFC83D4F2BB807D100750834 /* RoundsView.swift in Sources */,
FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */,
C4339BFB2CFF7D68004E5F09 /* UserSearchView.swift in Sources */,
FF8F26412BADFC8700650388 /* TournamentInitView.swift in Sources */,
C4A47D8A2B7BBB6500ADC637 /* SubscriptionView.swift in Sources */,
FFD655D82C8DE27400E5B35E /* TournamentLookUpView.swift in Sources */,
@ -2830,6 +2836,7 @@
FF4CBF992C996C0600151637 /* StoreManager.swift in Sources */,
FF4CBF9A2C996C0600151637 /* SearchViewModel.swift in Sources */,
FF4CBF9B2C996C0600151637 /* PlayerRegistration.swift in Sources */,
C4339BFD2CFF7D68004E5F09 /* UserSearchView.swift in Sources */,
FF4CBF9C2C996C0600151637 /* ImportedPlayerView.swift in Sources */,
FF4CBF9D2C996C0600151637 /* EditingTeamView.swift in Sources */,
FF4CBF9E2C996C0600151637 /* NetworkManagerError.swift in Sources */,
@ -3096,6 +3103,7 @@
FF70FB182C90584900129CC2 /* StoreManager.swift in Sources */,
FF70FB192C90584900129CC2 /* SearchViewModel.swift in Sources */,
FF70FB1A2C90584900129CC2 /* PlayerRegistration.swift in Sources */,
C4339BFC2CFF7D68004E5F09 /* UserSearchView.swift in Sources */,
FF70FB1B2C90584900129CC2 /* ImportedPlayerView.swift in Sources */,
FF70FB1C2C90584900129CC2 /* EditingTeamView.swift in Sources */,
FF70FB1D2C90584900129CC2 /* NetworkManagerError.swift in Sources */,

@ -19,7 +19,7 @@ class AppDelegate : NSObject, UIApplicationDelegate, UNUserNotificationCenterDel
UIApplication.shared.registerForRemoteNotifications()
UNUserNotificationCenter.current().delegate = self
return true
}

@ -20,4 +20,5 @@ enum Screen: String, Codable {
case broadcast
case event
case print
case share
}

@ -31,6 +31,12 @@ struct LabelDelete: View {
}
}
struct ShareLabel: View {
var body: some View {
Label("Partager", systemImage: "square.and.arrow.up.fill")
}
}
struct LabelFilter: View {
var body: some View {
Label("Filtrer", systemImage: "line.3.horizontal.decrease.circle")

@ -16,6 +16,8 @@ struct EventListView: View {
let tournaments: [FederalTournamentHolder]
let sortAscending: Bool
@State var showUserSearch: Bool = false
var body: some View {
let groupedTournamentsByDate = Dictionary(grouping: federalDataViewModel.filteredFederalTournaments(from: tournaments)) { $0.startDate.startOfMonth }
@ -118,6 +120,15 @@ struct EventListView: View {
private func _tournamentView(_ tournament: Tournament) -> some 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)
}
}
}
}
.contextMenu {
if tournament.hasEnded() == false {
@ -144,6 +155,12 @@ struct EventListView: View {
} label: {
LabelDelete()
}
Button() {
self.showUserSearch = true
} label: {
ShareLabel().tint(.orange)
}
}
#endif
}

@ -111,6 +111,14 @@ 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)
}
}
}
}
.environment(tournament)
@ -192,6 +200,10 @@ struct TournamentView: View {
NavigationLink(value: Screen.print) {
Label("Imprimer", systemImage: "printer")
}
NavigationLink(value: Screen.share) {
Label("Partager", systemImage: "square.and.arrow.up")
}
Divider()

@ -0,0 +1,168 @@
//
// 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