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
This commit is contained in:
275
Lendair/Models/Club.swift
Normal file
275
Lendair/Models/Club.swift
Normal file
@@ -0,0 +1,275 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user