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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user