RTSDA-iOS/Views/Detail/EventDetailShared.swift
RTSDA 00679f927c docs: Update README for v2.0 release and fix git remote URL
- Comprehensive README update documenting v2.0 architectural changes
- Updated git remote to ssh://rockvilleav@git.rockvilletollandsda.church:10443/RTSDA/RTSDA-iOS.git
- Documented unified ChurchService and 60% code reduction
- Added new features: Home Feed, responsive reading, enhanced UI
- Corrected license information (GPL v3 with church content copyright)
- Updated build instructions and technical stack details
2025-08-16 18:41:51 -04:00

308 lines
12 KiB
Swift

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<Bool>) {
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<Bool>, calendarMessage: Binding<String>) {
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<Bool>, calendarMessage: Binding<String>) {
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`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
}