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
276 lines
6.3 KiB
Swift
276 lines
6.3 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
// MARK: - Club
|
|
|
|
struct Club: Identifiable, Equatable, Codable {
|
|
let id: String
|
|
let name: String
|
|
let description: String
|
|
let clubType: ClubType
|
|
let privacy: ClubPrivacy
|
|
let location: String
|
|
let latitude: Double?
|
|
let longitude: Double?
|
|
var memberCount: Int
|
|
let maxMembers: Int?
|
|
let imageUrl: String?
|
|
let rules: String?
|
|
let ownerId: String
|
|
let ownerName: String
|
|
var membershipStatus: MembershipStatus
|
|
let createdAt: Date
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, name, description, clubType, privacy, location, latitude, longitude, memberCount, maxMembers, imageUrl, rules, ownerId, ownerName, membershipStatus, createdAt
|
|
}
|
|
|
|
init(
|
|
id: String,
|
|
name: String,
|
|
description: String,
|
|
clubType: ClubType,
|
|
privacy: ClubPrivacy,
|
|
location: String,
|
|
latitude: Double?,
|
|
longitude: Double?,
|
|
memberCount: Int,
|
|
maxMembers: Int?,
|
|
imageUrl: String?,
|
|
rules: String?,
|
|
ownerId: String,
|
|
ownerName: String,
|
|
membershipStatus: MembershipStatus,
|
|
createdAt: Date
|
|
) {
|
|
self.id = id
|
|
self.name = name
|
|
self.description = description
|
|
self.clubType = clubType
|
|
self.privacy = privacy
|
|
self.location = location
|
|
self.latitude = latitude
|
|
self.longitude = longitude
|
|
self.memberCount = memberCount
|
|
self.maxMembers = maxMembers
|
|
self.imageUrl = imageUrl
|
|
self.rules = rules
|
|
self.ownerId = ownerId
|
|
self.ownerName = ownerName
|
|
self.membershipStatus = membershipStatus
|
|
self.createdAt = createdAt
|
|
}
|
|
|
|
static func == (lhs: Club, rhs: Club) -> Bool {
|
|
lhs.id == rhs.id && lhs.membershipStatus == rhs.membershipStatus
|
|
}
|
|
|
|
var availableSpots: Int? {
|
|
guard let max = maxMembers else { return nil }
|
|
return max - memberCount
|
|
}
|
|
|
|
var isFull: Bool {
|
|
guard let max = maxMembers else { return false }
|
|
return memberCount >= max
|
|
}
|
|
}
|
|
|
|
// MARK: - Club Type
|
|
|
|
enum ClubType: String, CaseIterable, Codable {
|
|
case running
|
|
case walking
|
|
case cycling
|
|
case triathlon
|
|
case crossfit
|
|
case general
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .running: return "Running"
|
|
case .walking: return "Walking"
|
|
case .cycling: return "Cycling"
|
|
case .triathlon: return "Triathlon"
|
|
case .crossfit: return "CrossFit"
|
|
case .general: return "General Fitness"
|
|
}
|
|
}
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .running: return "figure.run"
|
|
case .walking: return "figure.walk"
|
|
case .cycling: return "bicycle"
|
|
case .triathlon: return "triangle.fill"
|
|
case .crossfit: return "dumbbell.fill"
|
|
case .general: return "heart.fill"
|
|
}
|
|
}
|
|
|
|
var color: Color {
|
|
switch self {
|
|
case .running: return .blue
|
|
case .walking: return .green
|
|
case .cycling: return .orange
|
|
case .triathlon: return .purple
|
|
case .crossfit: return .red
|
|
case .general: return .indigo
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Club Privacy
|
|
|
|
enum ClubPrivacy: String, CaseIterable, Codable {
|
|
case publicPrivacy
|
|
case privateClub
|
|
case invitationOnly
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .publicPrivacy: return "Public"
|
|
case .privateClub: return "Private"
|
|
case .invitationOnly: return "Invitation Only"
|
|
}
|
|
}
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .publicPrivacy: return "globe"
|
|
case .privateClub: return "lock.fill"
|
|
case .invitationOnly: return "mail.fill"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Membership Status
|
|
|
|
enum MembershipStatus: String, CaseIterable, Codable {
|
|
case active
|
|
case pending
|
|
case invited
|
|
case left
|
|
}
|
|
|
|
// MARK: - Club Member
|
|
|
|
struct ClubMember: Identifiable, Codable {
|
|
let id: String
|
|
let name: String
|
|
let avatarUrl: String?
|
|
let role: MemberRole
|
|
let joinedAt: Date
|
|
let membershipStatus: MembershipStatus
|
|
}
|
|
|
|
enum MemberRole: String, CaseIterable, Codable {
|
|
case owner
|
|
case admin
|
|
case member
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .owner: return "Owner"
|
|
case .admin: return "Admin"
|
|
case .member: return "Member"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Create Club Request
|
|
|
|
struct CreateClubRequest: Encodable {
|
|
let name: String
|
|
let description: String
|
|
let clubType: ClubType
|
|
let privacy: ClubPrivacy
|
|
let location: String
|
|
let latitude: Double?
|
|
let longitude: Double?
|
|
let maxMembers: Int?
|
|
let rules: String?
|
|
}
|
|
|
|
// MARK: - Update Club Request
|
|
|
|
struct UpdateClubRequest: Encodable {
|
|
var name: String?
|
|
var description: String?
|
|
var clubType: ClubType?
|
|
var privacy: ClubPrivacy?
|
|
var location: String?
|
|
var latitude: Double?
|
|
var longitude: Double?
|
|
var maxMembers: Int?
|
|
var rules: String?
|
|
}
|
|
|
|
// MARK: - Club Filter
|
|
|
|
struct ClubFilter: Encodable {
|
|
var clubType: ClubType?
|
|
var privacy: ClubPrivacy?
|
|
var membershipStatus: MembershipStatus?
|
|
var location: String?
|
|
var radiusKm: Double?
|
|
var limit: Int
|
|
var offset: Int
|
|
|
|
init(
|
|
clubType: ClubType? = nil,
|
|
privacy: ClubPrivacy? = nil,
|
|
membershipStatus: MembershipStatus? = nil,
|
|
location: String? = nil,
|
|
radiusKm: Double? = nil,
|
|
limit: Int = 20,
|
|
offset: Int = 0
|
|
) {
|
|
self.clubType = clubType
|
|
self.privacy = privacy
|
|
self.membershipStatus = membershipStatus
|
|
self.location = location
|
|
self.radiusKm = radiusKm
|
|
self.limit = limit
|
|
self.offset = offset
|
|
}
|
|
}
|
|
|
|
// MARK: - API Response Types
|
|
|
|
struct ClubListResponse: Decodable {
|
|
let clubs: [Club]
|
|
let hasMore: Bool
|
|
}
|
|
|
|
struct ClubDetailResponse: Decodable {
|
|
let club: Club
|
|
let members: [ClubMember]
|
|
}
|
|
|
|
struct CreateClubResponse: Decodable {
|
|
let club: Club
|
|
}
|
|
|
|
struct UpdateClubResponse: Decodable {
|
|
let club: Club
|
|
}
|
|
|
|
struct MembershipResponse: Decodable {
|
|
let success: Bool
|
|
let clubId: String
|
|
let status: MembershipStatus
|
|
}
|
|
|
|
struct InviteMemberResponse: Decodable {
|
|
let success: Bool
|
|
let clubId: String
|
|
let memberId: String
|
|
}
|
|
|
|
struct RemoveMemberResponse: Decodable {
|
|
let success: Bool
|
|
let clubId: String
|
|
let memberId: String
|
|
}
|