- Models: TrainingPlan, Race, FamilyPlan, BeginnerMode, CommunityEvent - Services: 5 service layers with protocol-based architecture - ViewModels: 5 view models with @MainActor ObservableObject pattern - Views: 10 SwiftUI views for all Phase 3 features - Updated README with full Phase 3 documentation Co-Authored-By: Paperclip <noreply@paperclip.ing>
184 lines
4.4 KiB
Swift
184 lines
4.4 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
// MARK: - Race
|
|
|
|
struct Race: Identifiable, Equatable, Codable {
|
|
let id: String
|
|
let name: String
|
|
let description: String
|
|
let location: String
|
|
let latitude: Double
|
|
let longitude: Double
|
|
let raceDate: Date
|
|
let distanceKm: Double
|
|
let raceType: RaceType
|
|
let organizerName: String
|
|
let registrationUrl: String?
|
|
let imageUrl: String?
|
|
let participantCount: Int?
|
|
let isRegistered: Bool
|
|
let isSaved: Bool
|
|
let elevationGain: Double
|
|
let terrainType: TerrainType
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, name, description, location, latitude, longitude, raceDate, distanceKm, raceType, organizerName, registrationUrl, imageUrl, participantCount, isRegistered, isSaved, elevationGain, terrainType
|
|
}
|
|
|
|
init(
|
|
id: String,
|
|
name: String,
|
|
description: String,
|
|
location: String,
|
|
latitude: Double,
|
|
longitude: Double,
|
|
raceDate: Date,
|
|
distanceKm: Double,
|
|
raceType: RaceType,
|
|
organizerName: String,
|
|
registrationUrl: String?,
|
|
imageUrl: String?,
|
|
participantCount: Int?,
|
|
isRegistered: Bool,
|
|
isSaved: Bool,
|
|
elevationGain: Double,
|
|
terrainType: TerrainType
|
|
) {
|
|
self.id = id
|
|
self.name = name
|
|
self.description = description
|
|
self.location = location
|
|
self.latitude = latitude
|
|
self.longitude = longitude
|
|
self.raceDate = raceDate
|
|
self.distanceKm = distanceKm
|
|
self.raceType = raceType
|
|
self.organizerName = organizerName
|
|
self.registrationUrl = registrationUrl
|
|
self.imageUrl = imageUrl
|
|
self.participantCount = participantCount
|
|
self.isRegistered = isRegistered
|
|
self.isSaved = isSaved
|
|
self.elevationGain = elevationGain
|
|
self.terrainType = terrainType
|
|
}
|
|
|
|
static func == (lhs: Race, rhs: Race) -> Bool {
|
|
lhs.id == rhs.id && lhs.isRegistered == rhs.isRegistered && lhs.isSaved == rhs.isSaved
|
|
}
|
|
|
|
var daysUntilRace: Int {
|
|
let calendar = Calendar.current
|
|
return calendar.dateComponents([.day], from: Date(), to: raceDate).day ?? 0
|
|
}
|
|
|
|
var isUpcoming: Bool {
|
|
raceDate > Date()
|
|
}
|
|
}
|
|
|
|
// MARK: - Race Type
|
|
|
|
enum RaceType: String, CaseIterable, Codable {
|
|
case road
|
|
case trail
|
|
case track
|
|
case virtual
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .road: return "Road"
|
|
case .trail: return "Trail"
|
|
case .track: return "Track"
|
|
case .virtual: return "Virtual"
|
|
}
|
|
}
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .road: return "car.fill"
|
|
case .trail: return "mountain.2.fill"
|
|
case .track: return "circle.fill"
|
|
case .virtual: return "globe"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Terrain Type
|
|
|
|
enum TerrainType: String, CaseIterable, Codable {
|
|
case flat
|
|
case rolling
|
|
case hilly
|
|
case mountainous
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .flat: return "Flat"
|
|
case .rolling: return "Rolling"
|
|
case .hilly: return "Hilly"
|
|
case .mountainous: return "Mountainous"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Race Filter
|
|
|
|
struct RaceFilter: Encodable {
|
|
var distanceKm: Double?
|
|
var raceType: RaceType?
|
|
var terrainType: TerrainType?
|
|
var startDate: Date?
|
|
var endDate: Date?
|
|
var location: String?
|
|
var radiusKm: Double?
|
|
var limit: Int
|
|
var offset: Int
|
|
|
|
init(
|
|
distanceKm: Double? = nil,
|
|
raceType: RaceType? = nil,
|
|
terrainType: TerrainType? = nil,
|
|
startDate: Date? = nil,
|
|
endDate: Date? = nil,
|
|
location: String? = nil,
|
|
radiusKm: Double? = nil,
|
|
limit: Int = 20,
|
|
offset: Int = 0
|
|
) {
|
|
self.distanceKm = distanceKm
|
|
self.raceType = raceType
|
|
self.terrainType = terrainType
|
|
self.startDate = startDate
|
|
self.endDate = endDate
|
|
self.location = location
|
|
self.radiusKm = radiusKm
|
|
self.limit = limit
|
|
self.offset = offset
|
|
}
|
|
}
|
|
|
|
// MARK: - API Response Types
|
|
|
|
struct RaceListResponse: Decodable {
|
|
let races: [Race]
|
|
let hasMore: Bool
|
|
}
|
|
|
|
struct RaceDetailResponse: Decodable {
|
|
let race: Race
|
|
}
|
|
|
|
struct SaveRaceResponse: Decodable {
|
|
let success: Bool
|
|
let raceId: String
|
|
let isSaved: Bool
|
|
}
|
|
|
|
struct RegisterRaceResponse: Decodable {
|
|
let success: Bool
|
|
let raceId: String
|
|
let registrationUrl: String?
|
|
}
|