Files
FrenoCorp/Lendair/ViewModels/ClubViewModel.swift
Senior Engineer 88d57a3389 Add Phase 2 community features: clubs and challenges (FRE-4664)
Implement full MVVM stack for two new community features:

Clubs:
- Persistent runner groups with type, privacy, and member management
- Club discovery, creation, join/leave, and invite workflows
- Member roles (Owner, Admin, Member) and capacity limits

Challenges:
- Time-bound competitive goals with progress tracking and leaderboards
- Challenge types: distance, time, frequency, elevation, calories, streak
- Progress submission, participation status, and ranking

Files:
- Models: Club.swift, Challenge.swift
- Services: ClubService.swift, ChallengeService.swift
- ViewModels: ClubViewModel.swift, ChallengeViewModel.swift
- Views: ClubsView.swift, ClubDetailView.swift, ChallengesView.swift, ChallengeDetailView.swift
- Tests: ClubServiceTests.swift, ChallengeServiceTests.swift
- Updated README.md with new feature documentation
2026-05-03 19:10:34 -04:00

157 lines
4.6 KiB
Swift

import Foundation
import SwiftUI
@MainActor
class ClubViewModel: ObservableObject {
@Published var clubs: [Club] = []
@Published var selectedClub: Club?
@Published var members: [ClubMember] = []
@Published var isLoading: Bool = false
@Published var error: ClubError?
@Published var filter: ClubFilter = ClubFilter()
private let service: ClubServiceProtocol
init(service: ClubServiceProtocol = ClubService()) {
self.service = service
}
func fetchClubs() async {
isLoading = true
error = nil
defer { isLoading = false }
do {
clubs = try await service.listClubs(filter: filter)
} catch let error as ClubError {
self.error = error
} catch {
print("Failed to fetch clubs: \(error)")
}
}
func selectClub(id: String) async {
isLoading = true
error = nil
defer { isLoading = false }
do {
let result = try await service.getClub(id: id)
selectedClub = result.club
members = result.members
if let index = clubs.firstIndex(where: { $0.id == id }) {
clubs[index] = result.club
objectWillChange.send()
}
} catch let error as ClubError {
self.error = error
} catch {
print("Failed to get club: \(error)")
}
}
func createClub(request: CreateClubRequest) async -> Club? {
isLoading = true
error = nil
defer { isLoading = false }
do {
let club = try await service.createClub(request: request)
clubs.insert(club, at: 0)
objectWillChange.send()
return club
} catch let error as ClubError {
self.error = error
return nil
} catch {
print("Failed to create club: \(error)")
return nil
}
}
func updateClub(id: String, request: UpdateClubRequest) async {
isLoading = true
error = nil
defer { isLoading = false }
do {
let updatedClub = try await service.updateClub(id: id, request: request)
if let index = clubs.firstIndex(where: { $0.id == id }) {
clubs[index] = updatedClub
objectWillChange.send()
}
if selectedClub?.id == id {
selectedClub = updatedClub
}
} catch let error as ClubError {
self.error = error
} catch {
print("Failed to update club: \(error)")
}
}
func joinClub(id: String) async {
do {
try await service.joinClub(id: id)
if let index = clubs.firstIndex(where: { $0.id == id }) {
clubs[index].membershipStatus = .active
clubs[index].memberCount += 1
objectWillChange.send()
}
} catch {
print("Failed to join club: \(error)")
}
}
func leaveClub(id: String) async {
do {
try await service.leaveClub(id: id)
if let index = clubs.firstIndex(where: { $0.id == id }) {
clubs[index].membershipStatus = .left
clubs[index].memberCount = max(0, clubs[index].memberCount - 1)
objectWillChange.send()
}
} catch {
print("Failed to leave club: \(error)")
}
}
func inviteMember(clubId: String, email: String) async {
do {
try await service.inviteMember(clubId: clubId, email: email)
} catch {
print("Failed to invite member: \(error)")
}
}
func removeMember(clubId: String, memberId: String) async {
do {
try await service.removeMember(clubId: clubId, memberId: memberId)
if let index = members.firstIndex(where: { $0.id == memberId }) {
members.remove(at: index)
if let clubIndex = clubs.firstIndex(where: { $0.id == clubId }) {
clubs[clubIndex].memberCount = max(0, clubs[clubIndex].memberCount - 1)
}
objectWillChange.send()
}
} catch {
print("Failed to remove member: \(error)")
}
}
var publicClubs: [Club] {
clubs.filter { $0.privacy == .publicPrivacy }
}
var userClubs: [Club] {
clubs.filter { $0.membershipStatus == .active }
}
var pendingClubs: [Club] {
clubs.filter { $0.membershipStatus == .pending }
}
var clubTypes: [ClubType] { ClubType.allCases }
var privacyOptions: [ClubPrivacy] { ClubPrivacy.allCases }
}