Files
FrenoCorp/Lendair/Models/Club.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

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
}