import SwiftUI struct CalendarView: View { @EnvironmentObject var dataManager: ClimbingDataManager @EnvironmentObject var themeManager: ThemeManager let sessions: [ClimbSession] @Binding var selectedMonth: Date @Binding var selectedDate: Date? let onNavigateToSession: (UUID) -> Void var calendar: Calendar { Calendar.current } var monthYearString: String { let formatter = DateFormatter() formatter.dateFormat = "MMMM yyyy" return formatter.string(from: selectedMonth) } var sessionsByDate: [Date: [ClimbSession]] { Dictionary(grouping: sessions) { session in calendar.startOfDay(for: session.date) } } var daysInMonth: [Date?] { guard let monthInterval = calendar.dateInterval(of: .month, for: selectedMonth), calendar.dateInterval(of: .weekOfMonth, for: monthInterval.start) != nil else { return [] } let days = calendar.generateDates( inside: monthInterval, matching: DateComponents(hour: 0, minute: 0, second: 0) ) let firstDayOfMonth = days.first ?? monthInterval.start let firstWeekday = calendar.component(.weekday, from: firstDayOfMonth) let offset = firstWeekday - 1 var paddedDays: [Date?] = Array(repeating: nil, count: offset) paddedDays.append(contentsOf: days.map { $0 as Date? }) let remainder = paddedDays.count % 7 if remainder != 0 { paddedDays.append(contentsOf: Array(repeating: nil, count: 7 - remainder)) } return paddedDays } var body: some View { ScrollView { VStack(spacing: 0) { if let activeSession = dataManager.activeSession, let gym = dataManager.gym(withId: activeSession.gymId) { ActiveSessionBanner(session: activeSession, gym: gym) .environmentObject(MusicService.shared) .padding(.horizontal, 16) .padding(.top, 8) .padding(.bottom, 16) } VStack(spacing: 8) { HStack { Button(action: { changeMonth(by: -1) }) { Image(systemName: "chevron.left") .font(.title2) .fontWeight(.semibold) .foregroundColor(themeManager.accentColor) } .frame(width: 44, height: 44) Spacer() Text(monthYearString) .font(.title3) .fontWeight(.semibold) Spacer() Button(action: { changeMonth(by: 1) }) { Image(systemName: "chevron.right") .font(.title2) .fontWeight(.semibold) .foregroundColor(themeManager.accentColor) } .frame(width: 44, height: 44) } Button(action: { let today = Date() selectedMonth = today selectedDate = today }) { Text("Today") .font(.subheadline) .fontWeight(.semibold) .foregroundColor(themeManager.contrastingTextColor) .padding(.horizontal, 20) .padding(.vertical, 8) .background(themeManager.accentColor) .clipShape(Capsule()) } } .padding(.vertical, 16) .padding(.horizontal) HStack(spacing: 0) { ForEach(["S", "M", "T", "W", "T", "F", "S"], id: \.self) { day in Text(day) .font(.caption2) .fontWeight(.semibold) .foregroundColor(.secondary) .frame(maxWidth: .infinity) } } .padding(.horizontal) .padding(.bottom, 8) LazyVGrid( columns: Array(repeating: GridItem(.flexible(), spacing: 4), count: 7), spacing: 4 ) { ForEach(daysInMonth.indices, id: \.self) { index in if let date = daysInMonth[index] { CalendarDayCell( date: date, sessions: sessionsByDate[calendar.startOfDay(for: date)] ?? [], isSelected: selectedDate.map { calendar.isDate($0, inSameDayAs: date) } ?? false, isToday: calendar.isDateInToday(date), isInCurrentMonth: calendar.isDate( date, equalTo: selectedMonth, toGranularity: .month) ) { if !sessionsByDate[calendar.startOfDay(for: date), default: []] .isEmpty { if selectedDate.map({ calendar.isDate($0, inSameDayAs: date) }) ?? false { selectedDate = nil } else { selectedDate = date } } } } else { Color.clear .aspectRatio(1, contentMode: .fit) } } } .padding(.horizontal) if let selected = selectedDate, let sessionsOnDate = sessionsByDate[calendar.startOfDay(for: selected)], !sessionsOnDate.isEmpty { Divider() .padding(.vertical, 16) .padding(.horizontal) VStack(alignment: .leading, spacing: 12) { Text("Sessions on \(formatSelectedDate(selected))") .font(.headline) .fontWeight(.semibold) .padding(.horizontal) VStack(spacing: 12) { ForEach(sessionsOnDate) { session in SessionCard( session: session, onTap: { onNavigateToSession(session.id) } ) .padding(.horizontal) } } } .padding(.bottom, 16) } } } } func changeMonth(by value: Int) { if let newMonth = calendar.date(byAdding: .month, value: value, to: selectedMonth) { selectedMonth = newMonth selectedDate = nil } } func formatSelectedDate(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "MMMM d, yyyy" return formatter.string(from: date) } } struct CalendarDayCell: View { let date: Date let sessions: [ClimbSession] let isSelected: Bool let isToday: Bool let isInCurrentMonth: Bool let onTap: () -> Void @EnvironmentObject var themeManager: ThemeManager var dayNumber: String { let formatter = DateFormatter() formatter.dateFormat = "d" return formatter.string(from: date) } var body: some View { Button(action: onTap) { VStack(spacing: 6) { Text(dayNumber) .font(.system(size: 17)) .fontWeight(sessions.isEmpty ? .regular : .medium) .foregroundColor( isSelected ? themeManager.contrastingTextColor : isToday ? themeManager.accentColor : !isInCurrentMonth ? .secondary.opacity(0.3) : sessions.isEmpty ? .secondary : .primary ) if !sessions.isEmpty { Circle() .fill(isSelected ? themeManager.contrastingTextColor : themeManager.accentColor) .frame(width: 4, height: 4) } else { Spacer() .frame(height: 4) } } .frame(maxWidth: .infinity) .frame(height: 50) .contentShape(Rectangle()) .background( RoundedRectangle(cornerRadius: 6) .fill( isSelected ? themeManager.accentColor : isToday ? themeManager.accentColor.opacity(0.1) : Color.clear ) ) .overlay( RoundedRectangle(cornerRadius: 6) .stroke( isToday && !isSelected ? themeManager.accentColor.opacity(0.3) : Color.clear, lineWidth: 1 ) ) } .buttonStyle(PlainButtonStyle()) .disabled(sessions.isEmpty) } } struct SessionCard: View { @EnvironmentObject var dataManager: ClimbingDataManager let session: ClimbSession let onTap: () -> Void var gym: Gym? { dataManager.gym(withId: session.gymId) } var body: some View { HStack(spacing: 12) { VStack(alignment: .leading, spacing: 6) { Text(gym?.name ?? "Unknown Gym") .font(.body) .fontWeight(.semibold) .foregroundColor(.primary) if let duration = session.duration { Text("Duration: \(duration) minutes") .font(.subheadline) .foregroundColor(.secondary) } if let notes = session.notes, !notes.isEmpty { Text(notes) .font(.subheadline) .foregroundColor(.secondary) .lineLimit(2) } } Spacer() Image(systemName: "chevron.right") .font(.body) .fontWeight(.semibold) .foregroundColor(Color(.tertiaryLabel)) } .padding(16) .background( RoundedRectangle(cornerRadius: 10) .fill(Color(.secondarySystemGroupedBackground)) ) .onTapGesture { onTap() } } } extension Calendar { func generateDates( inside interval: DateInterval, matching components: DateComponents ) -> [Date] { var dates: [Date] = [] dates.append(interval.start) enumerateDates( startingAfter: interval.start, matching: components, matchingPolicy: .nextTime ) { date, _, stop in if let date = date { if date < interval.end { dates.append(date) } else { stop = true } } } return dates } }