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