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:
209
Lendair/Views/CommunityEventDetailView.swift
Normal file
209
Lendair/Views/CommunityEventDetailView.swift
Normal file
@@ -0,0 +1,209 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CommunityEventDetailView: View {
|
||||
let event: CommunityEvent
|
||||
@StateObject private var viewModel = CommunityEventViewModel()
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
eventHeader
|
||||
eventInfoSection
|
||||
eventDescription
|
||||
rsvpSection
|
||||
participantsSection
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
.navigationTitle(event.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onAppear {
|
||||
Task {
|
||||
await viewModel.selectEvent(id: event.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var eventHeader: some View {
|
||||
VStack(spacing: 12) {
|
||||
HStack {
|
||||
Image(systemName: event.eventType.icon)
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(event.eventType.color)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(event.eventType.displayName)
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
if let distance = event.distanceKm {
|
||||
Text("\(distance) km \u2022 \(event.location)")
|
||||
} else {
|
||||
Text(event.location)
|
||||
}
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
HStack(spacing: 20) {
|
||||
infoItem(label: "Date", value: formatDate(event.startDate))
|
||||
infoItem(label: "Participants", value: "\(event.participantCount)")
|
||||
if let difficulty = event.difficulty {
|
||||
infoItem(label: "Difficulty", value: difficulty.displayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.secondary.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
|
||||
private func infoItem(label: String, value: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(value)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
}
|
||||
|
||||
private var eventInfoSection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Event Details")
|
||||
.font(.headline)
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
detailRow(label: "Organizer", value: event.organizerName)
|
||||
detailRow(label: "Location", value: event.location)
|
||||
detailRow(label: "Start", value: formatDateTime(event.startDate))
|
||||
detailRow(label: "End", value: formatDateTime(event.endDate))
|
||||
if let max = event.maxParticipants {
|
||||
detailRow(label: "Capacity", value: "\(event.participantCount)/\(max)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func detailRow(label: String, value: String) -> some View {
|
||||
HStack {
|
||||
Text(label)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: 100, alignment: .leading)
|
||||
Text(value)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
}
|
||||
|
||||
private var eventDescription: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("About This Event")
|
||||
.font(.headline)
|
||||
Text(event.description)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private var rsvpSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Your RSVP")
|
||||
.font(.headline)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
rsvpButton(title: "Going", icon: "checkmark.circle", status: .going, color: .green)
|
||||
rsvpButton(title: "Maybe", icon: "questionmark.circle", status: .maybe, color: .orange)
|
||||
rsvpButton(title: "Not Going", icon: "xmark.circle", status: .notGoing, color: .red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func rsvpButton(title: String, icon: String, status: RSVPStatus, color: Color) -> some View {
|
||||
Button {
|
||||
Task {
|
||||
await viewModel.RSVP(eventId: event.id, status: status)
|
||||
}
|
||||
} label: {
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: event.rsvpStatus == status ? "\(icon).fill" : icon)
|
||||
.foregroundColor(event.rsvpStatus == status ? color : .secondary)
|
||||
Text(title)
|
||||
.font(.caption)
|
||||
.foregroundColor(event.rsvpStatus == status ? color : .secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
.background(event.rsvpStatus == status ? color.opacity(0.15) : Color.secondary.opacity(0.08))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
private var participantsSection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Participants (\(event.participantCount))")
|
||||
.font(.headline)
|
||||
|
||||
ForEach(viewModel.participants) { participant in
|
||||
HStack(spacing: 12) {
|
||||
Circle()
|
||||
.fill(Color.secondary.opacity(0.2))
|
||||
.frame(width: 32, height: 32)
|
||||
Text(participant.name)
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
Text(participant.rsvpStatus.rawValue.capitalized)
|
||||
.font(.caption)
|
||||
.foregroundColor(participant.rsvpStatus == .going ? .green : .secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func formatDate(_ date: Date) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .none
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
private func formatDateTime(_ date: Date) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .short
|
||||
formatter.timeStyle = .short
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationView {
|
||||
CommunityEventDetailView(event: sampleEvent)
|
||||
}
|
||||
}
|
||||
|
||||
private var sampleEvent: CommunityEvent {
|
||||
CommunityEvent(
|
||||
id: "1",
|
||||
title: "Sunday Morning Group Run",
|
||||
description: "Join us for a friendly morning run through the park. All paces welcome!",
|
||||
eventType: .groupRun,
|
||||
location: "Central Park",
|
||||
latitude: 40.7851,
|
||||
longitude: -73.9683,
|
||||
startDate: Date().addingTimeInterval(7 * 24 * 3600),
|
||||
endDate: Date().addingTimeInterval(7 * 24 * 3600 + 3600),
|
||||
distanceKm: 10,
|
||||
organizerId: "user1",
|
||||
organizerName: "Running Club",
|
||||
maxParticipants: 50,
|
||||
participantCount: 23,
|
||||
imageUrl: nil,
|
||||
difficulty: .beginner,
|
||||
rsvpStatus: .pending,
|
||||
createdAt: Date()
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user