import Combine import SwiftUI struct SessionsView: View { @EnvironmentObject var dataManager: ClimbingDataManager @State private var showingAddSession = false var body: some View { NavigationView { Group { if dataManager.sessions.isEmpty && dataManager.activeSession == nil { EmptySessionsView() } else { SessionsList() } } .navigationTitle("Sessions") .navigationBarTitleDisplayMode(.automatic) .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { if dataManager.isSyncing { HStack(spacing: 2) { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .blue)) .scaleEffect(0.6) } .padding(.horizontal, 6) .padding(.vertical, 3) .background( Circle() .fill(.regularMaterial) ) .transition(.scale.combined(with: .opacity)) .animation( .easeInOut(duration: 0.2), value: dataManager.isSyncing ) } if dataManager.gyms.isEmpty { EmptyView() } else if dataManager.activeSession == nil { Button("Start Session") { if dataManager.gyms.count == 1 { dataManager.startSession(gymId: dataManager.gyms.first!.id) } else { showingAddSession = true } } } } } .sheet(isPresented: $showingAddSession) { AddEditSessionView() } } .navigationViewStyle(.stack) } } struct SessionsList: View { @EnvironmentObject var dataManager: ClimbingDataManager @State private var sessionToDelete: ClimbSession? private var completedSessions: [ClimbSession] { dataManager.sessions .filter { $0.status == .completed } .sorted { $0.date > $1.date } } var body: some View { List { // Active session banner section if let activeSession = dataManager.activeSession, let gym = dataManager.gym(withId: activeSession.gymId) { Section { ActiveSessionBanner(session: activeSession, gym: gym) .padding(.horizontal, 16) .listRowInsets(EdgeInsets(top: 16, leading: 0, bottom: 24, trailing: 0)) .listRowBackground(Color.clear) .listRowSeparator(.hidden) } } // Completed sessions section if !completedSessions.isEmpty { Section { ForEach(completedSessions) { session in NavigationLink(destination: SessionDetailView(sessionId: session.id)) { SessionRow(session: session) } .swipeActions(edge: .trailing, allowsFullSwipe: false) { Button(role: .destructive) { sessionToDelete = session } label: { Label("Delete", systemImage: "trash") } } } } header: { if dataManager.activeSession != nil { Text("Previous Sessions") .font(.headline) .fontWeight(.semibold) } } } } .listStyle(.insetGrouped) .alert("Delete Session", isPresented: .constant(sessionToDelete != nil)) { Button("Cancel", role: .cancel) { sessionToDelete = nil } Button("Delete", role: .destructive) { if let session = sessionToDelete { dataManager.deleteSession(session) sessionToDelete = nil } } } message: { Text( "Are you sure you want to delete this session? This will also delete all attempts associated with this session." ) } } } struct ActiveSessionBanner: View { let session: ClimbSession let gym: Gym @EnvironmentObject var dataManager: ClimbingDataManager @State private var currentTime = Date() @State private var navigateToDetail = false @State private var timer: Timer? var body: some View { HStack { VStack(alignment: .leading, spacing: 4) { HStack { Image(systemName: "play.fill") .foregroundColor(.green) .font(.caption) Text("Active Session") .font(.headline) .fontWeight(.bold) } Text(gym.name) .font(.subheadline) .foregroundColor(.secondary) if let startTime = session.startTime { Text(formatDuration(from: startTime, to: currentTime)) .font(.caption) .foregroundColor(.secondary) } } .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) .onTapGesture { navigateToDetail = true } Button(action: { dataManager.endSession(session.id) }) { Image(systemName: "stop.fill") .font(.system(size: 16, weight: .bold)) .foregroundColor(.white) .frame(width: 32, height: 32) .background(Color.red) .clipShape(Circle()) } .buttonStyle(PlainButtonStyle()) } .padding() .background( RoundedRectangle(cornerRadius: 12) .fill(.green.opacity(0.1)) .stroke(.green.opacity(0.3), lineWidth: 1) ) .onAppear { startTimer() } .onDisappear { stopTimer() } .navigationDestination(isPresented: $navigateToDetail) { SessionDetailView(sessionId: session.id) } } private func formatDuration(from start: Date, to end: Date) -> String { let interval = end.timeIntervalSince(start) let hours = Int(interval) / 3600 let minutes = Int(interval) % 3600 / 60 let seconds = Int(interval) % 60 if hours > 0 { return String(format: "%dh %dm %ds", hours, minutes, seconds) } else if minutes > 0 { return String(format: "%dm %ds", minutes, seconds) } else { return String(format: "%ds", seconds) } } private func startTimer() { timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in currentTime = Date() } } private func stopTimer() { timer?.invalidate() timer = nil } } struct SessionRow: View { let session: ClimbSession @EnvironmentObject var dataManager: ClimbingDataManager private var gym: Gym? { dataManager.gym(withId: session.gymId) } var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { Text(gym?.name ?? "Unknown Gym") .font(.headline) .fontWeight(.semibold) Spacer() Text(formatDate(session.date)) .font(.caption) .foregroundColor(.secondary) } if let duration = session.duration { Text("Duration: \(duration) minutes") .font(.subheadline) .foregroundColor(.secondary) } if let notes = session.notes, !notes.isEmpty { Text(notes) .font(.caption) .foregroundColor(.secondary) .lineLimit(2) } } .padding(.vertical, 8) } private func formatDate(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateStyle = .medium return formatter.string(from: date) } } struct EmptySessionsView: View { @EnvironmentObject var dataManager: ClimbingDataManager @State private var showingAddSession = false var body: some View { VStack(spacing: 20) { Spacer() Image(systemName: "figure.climbing") .font(.system(size: 60)) .foregroundColor(.secondary) VStack(spacing: 8) { Text(dataManager.gyms.isEmpty ? "No Gyms Available" : "No Sessions Yet") .font(.title2) .fontWeight(.bold) Text( dataManager.gyms.isEmpty ? "Add a gym first to start tracking your climbing sessions!" : "Start your first climbing session!" ) .font(.body) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal) } if !dataManager.gyms.isEmpty { Button("Start Session") { if dataManager.gyms.count == 1 { dataManager.startSession(gymId: dataManager.gyms.first!.id) } else { showingAddSession = true } } .buttonStyle(.borderedProminent) .controlSize(.large) } Spacer() } .sheet(isPresented: $showingAddSession) { AddEditSessionView() } } } #Preview { SessionsView() .environmentObject(ClimbingDataManager.preview) }