parent
62fd9c5610
commit
9ebdffddd7
@ -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…
Reference in new issue