FRE-4665: Implement Phase 3 AI training plans and premium features
- 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>
This commit is contained in:
201
Lendair/Models/BeginnerMode.swift
Normal file
201
Lendair/Models/BeginnerMode.swift
Normal file
@@ -0,0 +1,201 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Beginner Configuration
|
||||
|
||||
struct BeginnerConfig: Codable {
|
||||
var isEnabled: Bool
|
||||
var currentLevel: BeginnerLevel
|
||||
var completedOnboardingSteps: [OnboardingStep]
|
||||
var milestones: [Milestone]
|
||||
var shownTips: Set<String>
|
||||
var preferredMetric: MetricType
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case isEnabled, currentLevel, completedOnboardingSteps, milestones, shownTips, preferredMetric
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Beginner Level
|
||||
|
||||
enum BeginnerLevel: String, CaseIterable, Codable {
|
||||
case justStarted
|
||||
case gettingComfortable
|
||||
case buildingConsistency
|
||||
case progressing
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .justStarted: return "Just Started"
|
||||
case .gettingComfortable: return "Getting Comfortable"
|
||||
case .buildingConsistency: return "Building Consistency"
|
||||
case .progressing: return "Progressing"
|
||||
}
|
||||
}
|
||||
|
||||
var requiredWorkouts: Int {
|
||||
switch self {
|
||||
case .justStarted: return 0
|
||||
case .gettingComfortable: return 5
|
||||
case .buildingConsistency: return 15
|
||||
case .progressing: return 30
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .justStarted: return "sparkles"
|
||||
case .gettingComfortable: return "leaf.fill"
|
||||
case .buildingConsistency: return "chart.bar.fill"
|
||||
case .progressing: return "bolt.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Onboarding Step
|
||||
|
||||
enum OnboardingStep: String, CaseIterable, Codable {
|
||||
case profileSetup
|
||||
case goalSelection
|
||||
case firstActivity
|
||||
case inviteFriends
|
||||
case enableNotifications
|
||||
case choosePlan
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .profileSetup: return "Profile Setup"
|
||||
case .goalSelection: return "Set Goals"
|
||||
case .firstActivity: return "First Activity"
|
||||
case .inviteFriends: return "Invite Friends"
|
||||
case .enableNotifications: return "Enable Notifications"
|
||||
case .choosePlan: return "Choose a Plan"
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .profileSetup: return "Complete your profile with your name and photo"
|
||||
case .goalSelection: return "Tell us what you want to achieve"
|
||||
case .firstActivity: return "Record your first workout"
|
||||
case .inviteFriends: return "Invite friends to join Nessa"
|
||||
case .enableNotifications: return "Get reminders and updates"
|
||||
case .choosePlan: return "Pick a training plan to follow"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Milestone
|
||||
|
||||
struct Milestone: Identifiable, Codable {
|
||||
let id: String
|
||||
let title: String
|
||||
let description: String
|
||||
let icon: String
|
||||
let requirement: MilestoneRequirement
|
||||
var isCompleted: Bool
|
||||
var completedAt: Date?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, title, description, icon, requirement, isCompleted, completedAt
|
||||
}
|
||||
|
||||
init(
|
||||
id: String,
|
||||
title: String,
|
||||
description: String,
|
||||
icon: String,
|
||||
requirement: MilestoneRequirement,
|
||||
isCompleted: Bool,
|
||||
completedAt: Date?
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.icon = icon
|
||||
self.requirement = requirement
|
||||
self.isCompleted = isCompleted
|
||||
self.completedAt = completedAt
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Milestone Requirement
|
||||
|
||||
struct MilestoneRequirement: Codable {
|
||||
let type: RequirementType
|
||||
let targetValue: Double
|
||||
}
|
||||
|
||||
enum RequirementType: String, CaseIterable, Codable {
|
||||
case totalDistanceKm
|
||||
case totalWorkouts
|
||||
case consecutiveDays
|
||||
case weeklyConsistency
|
||||
case firstWorkout
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .totalDistanceKm: return "Total Distance"
|
||||
case .totalWorkouts: return "Total Workouts"
|
||||
case .consecutiveDays: return "Streak"
|
||||
case .weeklyConsistency: return "Weekly Consistency"
|
||||
case .firstWorkout: return "First Workout"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Tip
|
||||
|
||||
struct BeginnerTip: Identifiable, Codable {
|
||||
let id: String
|
||||
let context: TipContext
|
||||
let title: String
|
||||
let message: String
|
||||
var isShown: Bool
|
||||
}
|
||||
|
||||
enum TipContext: String, CaseIterable, Codable {
|
||||
case beforeWorkout
|
||||
case afterWorkout
|
||||
case dailyReminder
|
||||
case progressUpdate
|
||||
case restDay
|
||||
}
|
||||
|
||||
// MARK: - Metric Type
|
||||
|
||||
enum MetricType: String, CaseIterable, Codable {
|
||||
case distance
|
||||
case duration
|
||||
case pace
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .distance: return "Distance"
|
||||
case .duration: return "Duration"
|
||||
case .pace: return "Pace"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - API Response Types
|
||||
|
||||
struct BeginnerConfigResponse: Decodable {
|
||||
let config: BeginnerConfig
|
||||
}
|
||||
|
||||
struct UpdateBeginnerConfigRequest: Encodable {
|
||||
var isEnabled: Bool?
|
||||
var completedOnboardingSteps: [OnboardingStep]?
|
||||
var preferredMetric: MetricType?
|
||||
}
|
||||
|
||||
struct UpdateBeginnerConfigResponse: Decodable {
|
||||
let success: Bool
|
||||
let config: BeginnerConfig
|
||||
}
|
||||
|
||||
struct MilestoneProgressResponse: Decodable {
|
||||
let milestones: [Milestone]
|
||||
let currentLevel: BeginnerLevel
|
||||
}
|
||||
243
Lendair/Models/CommunityEvent.swift
Normal file
243
Lendair/Models/CommunityEvent.swift
Normal file
@@ -0,0 +1,243 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Community Event
|
||||
|
||||
struct CommunityEvent: Identifiable, Equatable, Codable {
|
||||
let id: String
|
||||
let title: String
|
||||
let description: String
|
||||
let eventType: EventType
|
||||
let location: String
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
let startDate: Date
|
||||
let endDate: Date
|
||||
let distanceKm: Double?
|
||||
let organizerId: String
|
||||
let organizerName: String
|
||||
let maxParticipants: Int?
|
||||
let participantCount: Int
|
||||
let imageUrl: String?
|
||||
let difficulty: Difficulty?
|
||||
var rsvpStatus: RSVPStatus
|
||||
let createdAt: Date
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, title, description, eventType, location, latitude, longitude, startDate, endDate, distanceKm, organizerId, organizerName, maxParticipants, participantCount, imageUrl, difficulty, rsvpStatus, createdAt
|
||||
}
|
||||
|
||||
init(
|
||||
id: String,
|
||||
title: String,
|
||||
description: String,
|
||||
eventType: EventType,
|
||||
location: String,
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
distanceKm: Double?,
|
||||
organizerId: String,
|
||||
organizerName: String,
|
||||
maxParticipants: Int?,
|
||||
participantCount: Int,
|
||||
imageUrl: String?,
|
||||
difficulty: Difficulty?,
|
||||
rsvpStatus: RSVPStatus,
|
||||
createdAt: Date
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.eventType = eventType
|
||||
self.location = location
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.startDate = startDate
|
||||
self.endDate = endDate
|
||||
self.distanceKm = distanceKm
|
||||
self.organizerId = organizerId
|
||||
self.organizerName = organizerName
|
||||
self.maxParticipants = maxParticipants
|
||||
self.participantCount = participantCount
|
||||
self.imageUrl = imageUrl
|
||||
self.difficulty = difficulty
|
||||
self.rsvpStatus = rsvpStatus
|
||||
self.createdAt = createdAt
|
||||
}
|
||||
|
||||
static func == (lhs: CommunityEvent, rhs: CommunityEvent) -> Bool {
|
||||
lhs.id == rhs.id && lhs.rsvpStatus == rhs.rsvpStatus
|
||||
}
|
||||
|
||||
var isUpcoming: Bool {
|
||||
startDate > Date()
|
||||
}
|
||||
|
||||
var isOngoing: Bool {
|
||||
Date() >= startDate && Date() <= endDate
|
||||
}
|
||||
|
||||
var isPast: Bool {
|
||||
endDate < Date()
|
||||
}
|
||||
|
||||
var availableSpots: Int? {
|
||||
guard let max = maxParticipants else { return nil }
|
||||
return max - participantCount
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Event Type
|
||||
|
||||
enum EventType: String, CaseIterable, Codable {
|
||||
case groupRun
|
||||
case race
|
||||
case workshop
|
||||
case socialGather
|
||||
case charityEvent
|
||||
case trainingCamp
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .groupRun: return "Group Run"
|
||||
case .race: return "Race"
|
||||
case .workshop: return "Workshop"
|
||||
case .socialGather: return "Social"
|
||||
case .charityEvent: return "Charity"
|
||||
case .trainingCamp: return "Training Camp"
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .groupRun: return "person.3.fill"
|
||||
case .race: return "flag.fill"
|
||||
case .workshop: return "lightbulb.fill"
|
||||
case .socialGather: return "cup.and.saucer.fill"
|
||||
case .charityEvent: return "heart.fill"
|
||||
case .trainingCamp: return "figure.run"
|
||||
}
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .groupRun: return .blue
|
||||
case .race: return .orange
|
||||
case .workshop: return .purple
|
||||
case .socialGather: return .green
|
||||
case .charityEvent: return .red
|
||||
case .trainingCamp: return .indigo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RSVP Status
|
||||
|
||||
enum RSVPStatus: String, CaseIterable, Codable {
|
||||
case going
|
||||
case maybe
|
||||
case notGoing
|
||||
case pending
|
||||
}
|
||||
|
||||
// MARK: - Event Participant
|
||||
|
||||
struct EventParticipant: Identifiable, Codable {
|
||||
let id: String
|
||||
let name: String
|
||||
let avatarUrl: String?
|
||||
let rsvpStatus: RSVPStatus
|
||||
let joinedAt: Date
|
||||
}
|
||||
|
||||
// MARK: - Create Event Request
|
||||
|
||||
struct CreateEventRequest: Encodable {
|
||||
let title: String
|
||||
let description: String
|
||||
let eventType: EventType
|
||||
let location: String
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
let startDate: Date
|
||||
let endDate: Date
|
||||
let distanceKm: Double?
|
||||
let maxParticipants: Int?
|
||||
let difficulty: Difficulty?
|
||||
}
|
||||
|
||||
// MARK: - Update Event Request
|
||||
|
||||
struct UpdateEventRequest: Encodable {
|
||||
var title: String?
|
||||
var description: String?
|
||||
var eventType: EventType?
|
||||
var location: String?
|
||||
var latitude: Double?
|
||||
var longitude: Double?
|
||||
var startDate: Date?
|
||||
var endDate: Date?
|
||||
var distanceKm: Double?
|
||||
var maxParticipants: Int?
|
||||
}
|
||||
|
||||
// MARK: - Event Filter
|
||||
|
||||
struct EventFilter: Encodable {
|
||||
var eventType: EventType?
|
||||
var startDate: Date?
|
||||
var endDate: Date?
|
||||
var location: String?
|
||||
var radiusKm: Double?
|
||||
var rsvpStatus: RSVPStatus?
|
||||
var limit: Int
|
||||
var offset: Int
|
||||
|
||||
init(
|
||||
eventType: EventType? = nil,
|
||||
startDate: Date? = nil,
|
||||
endDate: Date? = nil,
|
||||
location: String? = nil,
|
||||
radiusKm: Double? = nil,
|
||||
rsvpStatus: RSVPStatus? = nil,
|
||||
limit: Int = 20,
|
||||
offset: Int = 0
|
||||
) {
|
||||
self.eventType = eventType
|
||||
self.startDate = startDate
|
||||
self.endDate = endDate
|
||||
self.location = location
|
||||
self.radiusKm = radiusKm
|
||||
self.rsvpStatus = rsvpStatus
|
||||
self.limit = limit
|
||||
self.offset = offset
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - API Response Types
|
||||
|
||||
struct EventListResponse: Decodable {
|
||||
let events: [CommunityEvent]
|
||||
let hasMore: Bool
|
||||
}
|
||||
|
||||
struct EventDetailResponse: Decodable {
|
||||
let event: CommunityEvent
|
||||
let participants: [EventParticipant]
|
||||
}
|
||||
|
||||
struct CreateEventResponse: Decodable {
|
||||
let event: CommunityEvent
|
||||
}
|
||||
|
||||
struct UpdateEventResponse: Decodable {
|
||||
let event: CommunityEvent
|
||||
}
|
||||
|
||||
struct RSVPResponse: Decodable {
|
||||
let success: Bool
|
||||
let eventId: String
|
||||
let status: RSVPStatus
|
||||
}
|
||||
217
Lendair/Models/FamilyPlan.swift
Normal file
217
Lendair/Models/FamilyPlan.swift
Normal file
@@ -0,0 +1,217 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Family Plan
|
||||
|
||||
struct FamilyPlan: Identifiable, Equatable, Codable {
|
||||
let id: String
|
||||
let ownerId: String
|
||||
let ownerName: String
|
||||
let members: [FamilyMember]
|
||||
let maxMembers: Int
|
||||
let subscriptionStatus: SubscriptionStatus
|
||||
let renewalDate: Date?
|
||||
let createdAt: Date
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, ownerId, ownerName, members, maxMembers, subscriptionStatus, renewalDate, createdAt
|
||||
}
|
||||
|
||||
init(
|
||||
id: String,
|
||||
ownerId: String,
|
||||
ownerName: String,
|
||||
members: [FamilyMember],
|
||||
maxMembers: Int,
|
||||
subscriptionStatus: SubscriptionStatus,
|
||||
renewalDate: Date?,
|
||||
createdAt: Date
|
||||
) {
|
||||
self.id = id
|
||||
self.ownerId = ownerId
|
||||
self.ownerName = ownerName
|
||||
self.members = members
|
||||
self.maxMembers = maxMembers
|
||||
self.subscriptionStatus = subscriptionStatus
|
||||
self.renewalDate = renewalDate
|
||||
self.createdAt = createdAt
|
||||
}
|
||||
|
||||
static func == (lhs: FamilyPlan, rhs: FamilyPlan) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
var availableSlots: Int {
|
||||
maxMembers - members.count
|
||||
}
|
||||
|
||||
var isActive: Bool {
|
||||
subscriptionStatus == .active
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Family Member
|
||||
|
||||
struct FamilyMember: Identifiable, Equatable, Codable {
|
||||
let id: String
|
||||
let name: String
|
||||
let email: String
|
||||
let role: MemberRole
|
||||
let joinedAt: Date
|
||||
let avatarUrl: String?
|
||||
var isPrimary: Bool
|
||||
var totalDistanceKm: Double
|
||||
var totalWorkouts: Int
|
||||
var weeklyDistanceKm: Double
|
||||
var weeklyWorkouts: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name, email, role, joinedAt, avatarUrl, isPrimary, totalDistanceKm, totalWorkouts, weeklyDistanceKm, weeklyWorkouts
|
||||
}
|
||||
|
||||
init(
|
||||
id: String,
|
||||
name: String,
|
||||
email: String,
|
||||
role: MemberRole,
|
||||
joinedAt: Date,
|
||||
avatarUrl: String?,
|
||||
isPrimary: Bool,
|
||||
totalDistanceKm: Double,
|
||||
totalWorkouts: Int,
|
||||
weeklyDistanceKm: Double,
|
||||
weeklyWorkouts: Int
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.email = email
|
||||
self.role = role
|
||||
self.joinedAt = joinedAt
|
||||
self.avatarUrl = avatarUrl
|
||||
self.isPrimary = isPrimary
|
||||
self.totalDistanceKm = totalDistanceKm
|
||||
self.totalWorkouts = totalWorkouts
|
||||
self.weeklyDistanceKm = weeklyDistanceKm
|
||||
self.weeklyWorkouts = weeklyWorkouts
|
||||
}
|
||||
|
||||
static func == (lhs: FamilyMember, rhs: FamilyMember) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Member Role
|
||||
|
||||
enum MemberRole: String, CaseIterable, Codable {
|
||||
case owner
|
||||
case member
|
||||
case pending
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .owner: return "Owner"
|
||||
case .member: return "Member"
|
||||
case .pending: return "Pending"
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .owner: return "star.fill"
|
||||
case .member: return "person.fill"
|
||||
case .pending: return "person.crop.circle.badge.exclamationmark"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subscription Status
|
||||
|
||||
enum SubscriptionStatus: String, CaseIterable, Codable {
|
||||
case active
|
||||
case expired
|
||||
case cancelled
|
||||
case pending
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .active: return "Active"
|
||||
case .expired: return "Expired"
|
||||
case .cancelled: return "Cancelled"
|
||||
case .pending: return "Pending"
|
||||
}
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .active: return .green
|
||||
case .expired: return .red
|
||||
case .cancelled: return .orange
|
||||
case .pending: return .yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Family Leaderboard Entry
|
||||
|
||||
struct FamilyLeaderboardEntry: Identifiable, Codable {
|
||||
let id: String
|
||||
let memberId: String
|
||||
let memberName: String
|
||||
let avatarUrl: String?
|
||||
let metric: LeaderboardMetric
|
||||
let value: Double
|
||||
let rank: Int
|
||||
}
|
||||
|
||||
// MARK: - Leaderboard Metric
|
||||
|
||||
enum LeaderboardMetric: String, CaseIterable, Codable {
|
||||
case distance
|
||||
case workouts
|
||||
case streak
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .distance: return "Distance"
|
||||
case .workouts: return "Workouts"
|
||||
case .streak: return "Streak"
|
||||
}
|
||||
}
|
||||
|
||||
var unit: String {
|
||||
switch self {
|
||||
case .distance: return "km"
|
||||
case .workouts: return ""
|
||||
case .streak: return "days"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Invite Member Request
|
||||
|
||||
struct InviteMemberRequest: Encodable {
|
||||
let email: String
|
||||
let name: String
|
||||
}
|
||||
|
||||
// MARK: - API Response Types
|
||||
|
||||
struct FamilyPlanDetailResponse: Decodable {
|
||||
let plan: FamilyPlan
|
||||
}
|
||||
|
||||
struct InviteMemberResponse: Decodable {
|
||||
let success: Bool
|
||||
let invitationId: String
|
||||
let memberEmail: String
|
||||
}
|
||||
|
||||
struct RemoveMemberResponse: Decodable {
|
||||
let success: Bool
|
||||
let memberId: String
|
||||
}
|
||||
|
||||
struct FamilyLeaderboardResponse: Decodable {
|
||||
let entries: [FamilyLeaderboardEntry]
|
||||
let metric: LeaderboardMetric
|
||||
}
|
||||
183
Lendair/Models/Race.swift
Normal file
183
Lendair/Models/Race.swift
Normal file
@@ -0,0 +1,183 @@
|
||||
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?
|
||||
}
|
||||
312
Lendair/Models/TrainingPlan.swift
Normal file
312
Lendair/Models/TrainingPlan.swift
Normal file
@@ -0,0 +1,312 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Training Plan
|
||||
|
||||
struct TrainingPlan: Identifiable, Equatable, Codable {
|
||||
let id: String
|
||||
let title: String
|
||||
let description: String
|
||||
let planType: PlanType
|
||||
let durationWeeks: Int
|
||||
let difficulty: Difficulty
|
||||
let startDate: Date
|
||||
let endDate: Date
|
||||
let weeklyWorkouts: [WeeklyWorkout]
|
||||
var progress: PlanProgress
|
||||
var isFollowing: Bool
|
||||
let createdAt: Date
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, title, description, planType, durationWeeks, difficulty, startDate, endDate, weeklyWorkouts, progress, isFollowing, createdAt
|
||||
}
|
||||
|
||||
init(
|
||||
id: String,
|
||||
title: String,
|
||||
description: String,
|
||||
planType: PlanType,
|
||||
durationWeeks: Int,
|
||||
difficulty: Difficulty,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
weeklyWorkouts: [WeeklyWorkout],
|
||||
progress: PlanProgress,
|
||||
isFollowing: Bool,
|
||||
createdAt: Date
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.planType = planType
|
||||
self.durationWeeks = durationWeeks
|
||||
self.difficulty = difficulty
|
||||
self.startDate = startDate
|
||||
self.endDate = endDate
|
||||
self.weeklyWorkouts = weeklyWorkouts
|
||||
self.progress = progress
|
||||
self.isFollowing = isFollowing
|
||||
self.createdAt = createdAt
|
||||
}
|
||||
|
||||
static func == (lhs: TrainingPlan, rhs: TrainingPlan) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Plan Type
|
||||
|
||||
enum PlanType: String, CaseIterable, Codable {
|
||||
case fiveK = "5K"
|
||||
case tenK = "10K"
|
||||
case halfMarathon = "HALF_MARATHON"
|
||||
case fullMarathon = "FULL_MARATHON"
|
||||
case custom = "CUSTOM"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .fiveK: return "5K"
|
||||
case .tenK: return "10K"
|
||||
case .halfMarathon: return "Half Marathon"
|
||||
case .fullMarathon: return "Full Marathon"
|
||||
case .custom: return "Custom"
|
||||
}
|
||||
}
|
||||
|
||||
var distanceKm: Double {
|
||||
switch self {
|
||||
case .fiveK: return 5.0
|
||||
case .tenK: return 10.0
|
||||
case .halfMarathon: return 21.1
|
||||
case .fullMarathon: return 42.2
|
||||
case .custom: return 0.0
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .fiveK: return "figure.run"
|
||||
case .tenK: return "figure.run"
|
||||
case .halfMarathon: return "flag.fill"
|
||||
case .fullMarathon: return "flag.fill"
|
||||
case .custom: return "wrench.and.screwdriver"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Difficulty
|
||||
|
||||
enum Difficulty: String, CaseIterable, Codable {
|
||||
case beginner
|
||||
case intermediate
|
||||
case advanced
|
||||
case elite
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .beginner: return "Beginner"
|
||||
case .intermediate: return "Intermediate"
|
||||
case .advanced: return "Advanced"
|
||||
case .elite: return "Elite"
|
||||
}
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .beginner: return .green
|
||||
case .intermediate: return .blue
|
||||
case .advanced: return .orange
|
||||
case .elite: return .red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Weekly Workout
|
||||
|
||||
struct WeeklyWorkout: Identifiable, Codable {
|
||||
let id: String
|
||||
let weekNumber: Int
|
||||
let dailySessions: [DailySession]
|
||||
|
||||
var completedSessions: Int {
|
||||
dailySessions.filter { $0.status == .completed }.count
|
||||
}
|
||||
|
||||
var totalSessions: Int {
|
||||
dailySessions.count
|
||||
}
|
||||
|
||||
var progressPercentage: Double {
|
||||
totalSessions == 0 ? 0 : Double(completedSessions) / Double(totalSessions) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Daily Session
|
||||
|
||||
struct DailySession: Identifiable, Codable {
|
||||
let id: String
|
||||
let dayOfWeek: DayOfWeek
|
||||
let workoutType: WorkoutType
|
||||
let title: String
|
||||
let description: String
|
||||
let targetDistanceKm: Double?
|
||||
let targetDurationMinutes: Int?
|
||||
let targetPaceMinPerKm: Double?
|
||||
let intensity: Intensity
|
||||
var status: SessionStatus
|
||||
var completedDistanceKm: Double?
|
||||
var completedDurationMinutes: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, dayOfWeek, workoutType, title, description, targetDistanceKm, targetDurationMinutes, targetPaceMinPerKm, intensity, status, completedDistanceKm, completedDurationMinutes
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Day of Week
|
||||
|
||||
enum DayOfWeek: String, CaseIterable, Codable {
|
||||
case monday, tuesday, wednesday, thursday, friday, saturday, sunday
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .monday: return "Mon"
|
||||
case .tuesday: return "Tue"
|
||||
case .wednesday: return "Wed"
|
||||
case .thursday: return "Thu"
|
||||
case .friday: return "Fri"
|
||||
case .saturday: return "Sat"
|
||||
case .sunday: return "Sun"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Workout Type
|
||||
|
||||
enum WorkoutType: String, CaseIterable, Codable {
|
||||
case easyRun
|
||||
case tempoRun
|
||||
case intervalTraining
|
||||
case longRun
|
||||
case speedWork
|
||||
case recoveryRun
|
||||
case crossTraining
|
||||
case rest
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .easyRun: return "Easy Run"
|
||||
case .tempoRun: return "Tempo Run"
|
||||
case .intervalTraining: return "Intervals"
|
||||
case .longRun: return "Long Run"
|
||||
case .speedWork: return "Speed Work"
|
||||
case .recoveryRun: return "Recovery Run"
|
||||
case .crossTraining: return "Cross Train"
|
||||
case .rest: return "Rest"
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .easyRun: return "figure.walk"
|
||||
case .tempoRun: return "figure.run"
|
||||
case .intervalTraining: return "bolt.fill"
|
||||
case .longRun: return "figure.run"
|
||||
case .speedWork: return "speedometer"
|
||||
case .recoveryRun: return "leaf.fill"
|
||||
case .crossTraining: return "dumbbell.fill"
|
||||
case .rest: return "moon.fill"
|
||||
}
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .easyRun: return .green
|
||||
case .tempoRun: return .blue
|
||||
case .intervalTraining: return .purple
|
||||
case .longRun: return .orange
|
||||
case .speedWork: return .red
|
||||
case .recoveryRun: return .mint
|
||||
case .crossTraining: return .gray
|
||||
case .rest: return .secondary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Intensity
|
||||
|
||||
enum Intensity: String, CaseIterable, Codable {
|
||||
case veryEasy
|
||||
case easy
|
||||
case moderate
|
||||
case hard
|
||||
case veryHard
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .veryEasy: return "Very Easy"
|
||||
case .easy: return "Easy"
|
||||
case .moderate: return "Moderate"
|
||||
case .hard: return "Hard"
|
||||
case .veryHard: return "Very Hard"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Session Status
|
||||
|
||||
enum SessionStatus: String, CaseIterable, Codable {
|
||||
case pending
|
||||
case inProgress
|
||||
case completed
|
||||
case skipped
|
||||
}
|
||||
|
||||
// MARK: - Plan Progress
|
||||
|
||||
struct PlanProgress: Codable {
|
||||
let completedWeeks: Int
|
||||
let totalWeeks: Int
|
||||
let completedSessions: Int
|
||||
let totalSessions: Int
|
||||
let currentWeekNumber: Int
|
||||
|
||||
var percentage: Double {
|
||||
totalWeeks == 0 ? 0 : Double(completedWeeks) / Double(totalWeeks) * 100
|
||||
}
|
||||
|
||||
var sessionPercentage: Double {
|
||||
totalSessions == 0 ? 0 : Double(completedSessions) / Double(totalSessions) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AI Plan Generation Request
|
||||
|
||||
struct GeneratePlanRequest: Encodable {
|
||||
let planType: PlanType
|
||||
let difficulty: Difficulty
|
||||
let startDate: Date
|
||||
let currentWeeklyMileageKm: Double?
|
||||
let goalTimeMinutes: Int?
|
||||
let availableDays: [DayOfWeek]
|
||||
}
|
||||
|
||||
// MARK: - API Response Types
|
||||
|
||||
struct TrainingPlanListResponse: Decodable {
|
||||
let plans: [TrainingPlan]
|
||||
let hasMore: Bool
|
||||
}
|
||||
|
||||
struct TrainingPlanDetailResponse: Decodable {
|
||||
let plan: TrainingPlan
|
||||
}
|
||||
|
||||
struct GeneratePlanResponse: Decodable {
|
||||
let plan: TrainingPlan
|
||||
}
|
||||
|
||||
struct UpdateSessionStatusResponse: Decodable {
|
||||
let success: Bool
|
||||
let sessionId: String
|
||||
let status: SessionStatus
|
||||
}
|
||||
Reference in New Issue
Block a user