import SwiftUI #if canImport(EventKit) import EventKit #endif // MARK: - Shared Components struct EventActionButtons: View { let event: ChurchEvent @Binding var showingDirections: Bool @Binding var showingShareSheet: Bool @Binding var showingCalendarAlert: Bool @Binding var calendarMessage: String let style: ActionButtonStyle enum ActionButtonStyle { case iphone case ipad } var body: some View { VStack(spacing: 12) { if !event.location.isEmpty { Button(action: { EventDetailActions.handleDirections(for: event, showingDirections: $showingDirections) }) { HStack { Image(systemName: EventDetailActions.isOnlineEvent(event) ? "link" : "location.fill") Text(EventDetailActions.isOnlineEvent(event) ? "Join Event" : "Get Directions") } .applyButtonStyle(style) .foregroundColor(.white) .if(style == .ipad) { view in view.background(Color.blue, in: Capsule()) } .if(style == .iphone) { view in view.background(Color.blue, in: RoundedRectangle(cornerRadius: 10)) } } } #if canImport(EventKit) Button(action: { EventDetailActions.addToCalendar( event: event, showingCalendarAlert: $showingCalendarAlert, calendarMessage: $calendarMessage ) }) { HStack { Image(systemName: "calendar.badge.plus") Text("Add to Calendar") } .applyButtonStyle(style) .foregroundColor(.white) .if(style == .ipad) { view in view.background(Color.orange, in: Capsule()) } .if(style == .iphone) { view in view.background(Color.orange, in: RoundedRectangle(cornerRadius: 10)) } } #endif #if os(iOS) Button(action: { DispatchQueue.main.async { showingShareSheet = true } }) { HStack { Image(systemName: "square.and.arrow.up") Text("Share Event") } .applyButtonStyle(style) .foregroundColor(.primary) .if(style == .ipad) { view in view.background(.thickMaterial, in: Capsule()) } .if(style == .iphone) { view in view.background(Color.gray.opacity(0.2), in: RoundedRectangle(cornerRadius: 10)) } } #endif } } } // MARK: - Shared Actions struct EventDetailActions { static func isOnlineEvent(_ event: ChurchEvent) -> Bool { let onlineKeywords = ["zoom", "whatsapp", "online", "virtual", "webinar", "meeting", "call"] return onlineKeywords.contains { keyword in event.location.lowercased().contains(keyword) } } static func handleDirections(for event: ChurchEvent, showingDirections: Binding) { if isOnlineEvent(event) { if let locationUrl = event.locationUrl, let url = URL(string: locationUrl) { UIApplication.shared.open(url) } else { showingDirections.wrappedValue = true } } else { let encodedLocation = event.location.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" if let url = URL(string: "http://maps.apple.com/?q=\(encodedLocation)") { UIApplication.shared.open(url) } } } #if canImport(EventKit) static func addToCalendar(event: ChurchEvent, showingCalendarAlert: Binding, calendarMessage: Binding) { let eventStore = EKEventStore() if #available(iOS 17.0, *) { eventStore.requestWriteOnlyAccessToEvents { granted, error in handleCalendarAccess(granted: granted, error: error, eventStore: eventStore, event: event, showingCalendarAlert: showingCalendarAlert, calendarMessage: calendarMessage) } } else { eventStore.requestAccess(to: .event) { granted, error in handleCalendarAccess(granted: granted, error: error, eventStore: eventStore, event: event, showingCalendarAlert: showingCalendarAlert, calendarMessage: calendarMessage) } } } private static func handleCalendarAccess(granted: Bool, error: Error?, eventStore: EKEventStore, event: ChurchEvent, showingCalendarAlert: Binding, calendarMessage: Binding) { DispatchQueue.main.async { if granted && error == nil { // Use Rust function to parse event data (RTSDA Architecture Rules compliance) guard let eventJson = try? JSONEncoder().encode(event), let jsonString = String(data: eventJson, encoding: .utf8) else { calendarMessage.wrappedValue = "Failed to process event data." showingCalendarAlert.wrappedValue = true return } let calendarDataJson = createCalendarEventData(eventJson: jsonString) let parsedDataJson = parseCalendarEventData(calendarJson: calendarDataJson) guard let data = parsedDataJson.data(using: .utf8), let calendarData = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { calendarMessage.wrappedValue = "Failed to parse calendar response" showingCalendarAlert.wrappedValue = true return } guard let success = calendarData["success"] as? Bool, success else { let errorMsg = calendarData["error"] as? String ?? "Failed to parse event dates" calendarMessage.wrappedValue = errorMsg showingCalendarAlert.wrappedValue = true return } let calendarEvent = EKEvent(eventStore: eventStore) calendarEvent.title = calendarData["title"] as? String ?? event.title calendarEvent.notes = calendarData["description"] as? String ?? event.description calendarEvent.location = calendarData["location"] as? String ?? event.location // Use parsed timestamps from Rust if let startTimestamp = calendarData["start_timestamp"] as? TimeInterval, let endTimestamp = calendarData["end_timestamp"] as? TimeInterval { calendarEvent.startDate = Date(timeIntervalSince1970: startTimestamp) calendarEvent.endDate = Date(timeIntervalSince1970: endTimestamp) } else { calendarMessage.wrappedValue = "Failed to parse event timestamps." showingCalendarAlert.wrappedValue = true return } // Add recurrence rule if event is recurring if let hasRecurrence = calendarData["has_recurrence"] as? Bool, hasRecurrence, let recurringType = calendarData["recurring_type"] as? String { if let recurrenceRule = createRecurrenceRule(from: recurringType) { calendarEvent.recurrenceRules = [recurrenceRule] } } calendarEvent.calendar = eventStore.defaultCalendarForNewEvents do { // Use .futureEvents for recurring events, .thisEvent for single events let span: EKSpan = (calendarData["has_recurrence"] as? Bool == true) ? .futureEvents : .thisEvent try eventStore.save(calendarEvent, span: span) let message = (calendarData["has_recurrence"] as? Bool == true) ? "Recurring event series successfully added to your calendar!" : "Event successfully added to your calendar!" calendarMessage.wrappedValue = message showingCalendarAlert.wrappedValue = true } catch { calendarMessage.wrappedValue = "Failed to add event to calendar. Please try again." showingCalendarAlert.wrappedValue = true } } else { calendarMessage.wrappedValue = "Calendar access is required to add events. Please enable it in Settings." showingCalendarAlert.wrappedValue = true } } } private static func createRecurrenceRule(from recurringType: String) -> EKRecurrenceRule? { switch recurringType.uppercased() { case "DAILY": // Daily Prayer Meeting: Sunday-Friday (exclude Saturday) let weekdays = [ EKRecurrenceDayOfWeek(.sunday), EKRecurrenceDayOfWeek(.monday), EKRecurrenceDayOfWeek(.tuesday), EKRecurrenceDayOfWeek(.wednesday), EKRecurrenceDayOfWeek(.thursday), EKRecurrenceDayOfWeek(.friday) ] return EKRecurrenceRule( recurrenceWith: .weekly, interval: 1, daysOfTheWeek: weekdays, daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil, end: EKRecurrenceEnd(end: Calendar.current.date(byAdding: .year, value: 1, to: Date()) ?? Date()) ) case "WEEKLY": return EKRecurrenceRule( recurrenceWith: .weekly, interval: 1, end: EKRecurrenceEnd(end: Calendar.current.date(byAdding: .year, value: 1, to: Date()) ?? Date()) ) case "BIWEEKLY": return EKRecurrenceRule( recurrenceWith: .weekly, interval: 2, end: EKRecurrenceEnd(end: Calendar.current.date(byAdding: .year, value: 1, to: Date()) ?? Date()) ) case "MONTHLY": return EKRecurrenceRule( recurrenceWith: .monthly, interval: 1, end: EKRecurrenceEnd(end: Calendar.current.date(byAdding: .year, value: 1, to: Date()) ?? Date()) ) case "FIRST_TUESDAY": let dayOfWeek = EKRecurrenceDayOfWeek(.tuesday) return EKRecurrenceRule( recurrenceWith: .monthly, interval: 1, daysOfTheWeek: [dayOfWeek], daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: [1], end: EKRecurrenceEnd(end: Calendar.current.date(byAdding: .year, value: 1, to: Date()) ?? Date()) ) default: return nil } } #endif static func createShareText(for event: ChurchEvent) -> String { return """ \(event.title) \(event.description) 📅 \(event.isMultiDay ? event.formattedDateTime : event.formattedDateRange) 🕐 \(event.formattedTime) 📍 \(event.location) Join us at Rockville Tolland SDA Church! """ } } // MARK: - Helper Extensions extension View { func applyButtonStyle(_ style: EventActionButtons.ActionButtonStyle) -> AnyView { switch style { case .iphone: return AnyView( self .frame(maxWidth: .infinity) .padding() ) case .ipad: return AnyView( self .font(.headline) .padding(.horizontal, 24) .padding(.vertical, 12) ) } } @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { if condition { transform(self) } else { self } } }