
- 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
455 lines
19 KiB
Swift
455 lines
19 KiB
Swift
import SwiftUI
|
|
import MapKit
|
|
import CoreLocation
|
|
|
|
struct ConnectView: View {
|
|
@Environment(ChurchDataService.self) private var dataService
|
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
@State private var churchConfig: ChurchConfig?
|
|
@State private var showingContactForm = false
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
LazyVStack(spacing: 0) {
|
|
// Hero Section
|
|
VStack(spacing: 24) {
|
|
VStack(spacing: 16) {
|
|
Text(getChurchName())
|
|
.font(.system(size: horizontalSizeClass == .regular ? 42 : 32, weight: .bold, design: .serif))
|
|
.foregroundColor(.primary)
|
|
|
|
Text("Proclaiming the Three Angels' Messages")
|
|
.font(.system(size: horizontalSizeClass == .regular ? 18 : 16, weight: .medium))
|
|
.foregroundColor(Color(hex: getBrandColor()))
|
|
.italic()
|
|
}
|
|
.padding(.top, 32)
|
|
}
|
|
.padding(.bottom, 40)
|
|
|
|
// Three Angels Messages
|
|
VStack(alignment: .leading, spacing: 24) {
|
|
Text("The Three Angels' Messages")
|
|
.font(.system(size: horizontalSizeClass == .regular ? 32 : 28, weight: .bold, design: .serif))
|
|
.padding(.horizontal, 20)
|
|
|
|
Text("Our mission is centered on the prophetic messages of Revelation 14, calling people to worship God, proclaim His truth, and prepare for Christ's second coming.")
|
|
.font(.system(size: horizontalSizeClass == .regular ? 18 : 16, weight: .regular))
|
|
.lineSpacing(4)
|
|
.padding(.horizontal, 20)
|
|
.foregroundColor(.secondary)
|
|
|
|
if horizontalSizeClass == .regular {
|
|
// iPad: Horizontal layout
|
|
HStack(spacing: 16) {
|
|
ThreeAngelsMessagesView()
|
|
}
|
|
.padding(.horizontal, 20)
|
|
} else {
|
|
// iPhone: Vertical layout
|
|
VStack(spacing: 16) {
|
|
ThreeAngelsMessagesView()
|
|
}
|
|
.padding(.horizontal, 20)
|
|
}
|
|
}
|
|
.padding(.bottom, 40)
|
|
|
|
// Worship With Us Section
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text("Worship With Us")
|
|
.font(.system(size: horizontalSizeClass == .regular ? 28 : 24, weight: .bold))
|
|
.padding(.horizontal, 20)
|
|
|
|
ServiceTimesSection()
|
|
.padding(20)
|
|
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
|
|
.padding(.horizontal, 20)
|
|
}
|
|
.padding(.bottom, 32)
|
|
|
|
// Mission Statement
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text("Our Mission")
|
|
.font(.system(size: horizontalSizeClass == .regular ? 28 : 24, weight: .bold))
|
|
.padding(.horizontal, 20)
|
|
|
|
Text(getAboutText())
|
|
.font(.system(size: horizontalSizeClass == .regular ? 18 : 16, weight: .regular))
|
|
.lineSpacing(4)
|
|
.padding(24)
|
|
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
|
|
.padding(.horizontal, 20)
|
|
|
|
NavigationLink {
|
|
BeliefsView()
|
|
} label: {
|
|
HStack {
|
|
Text("Our 28 Fundamental Beliefs")
|
|
.font(.system(size: horizontalSizeClass == .regular ? 18 : 16, weight: .semibold))
|
|
Spacer()
|
|
Image(systemName: "chevron.right")
|
|
}
|
|
.foregroundColor(.white)
|
|
.padding(20)
|
|
.background(Color(hex: getBrandColor()), in: RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
.padding(.horizontal, 20)
|
|
}
|
|
.padding(.bottom, 32)
|
|
|
|
// Visit & Connect Section
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text("Visit & Connect")
|
|
.font(.system(size: horizontalSizeClass == .regular ? 28 : 24, weight: .bold))
|
|
.padding(.horizontal, 20)
|
|
|
|
// Interactive Map
|
|
ChurchMapView()
|
|
.padding(.horizontal, 20)
|
|
|
|
// Contact Information
|
|
VStack(spacing: 12) {
|
|
ContactActionRow(
|
|
icon: "location.fill",
|
|
title: "Visit Us",
|
|
subtitle: getChurchAddress(),
|
|
iconColor: .red,
|
|
action: ContactActions.directionsAction(address: getChurchAddress().replacingOccurrences(of: " ", with: "+"))
|
|
)
|
|
|
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
|
ContactActionRow(
|
|
icon: "phone.fill",
|
|
title: "Call Us",
|
|
subtitle: getContactPhone(),
|
|
iconColor: .green,
|
|
action: ContactActions.callAction(phoneNumber: getContactPhone())
|
|
)
|
|
}
|
|
|
|
ContactActionRow(
|
|
icon: "envelope.fill",
|
|
title: "Email Us",
|
|
subtitle: getContactEmail(),
|
|
iconColor: .blue,
|
|
action: ContactActions.emailAction(email: getContactEmail())
|
|
)
|
|
}
|
|
.padding(20)
|
|
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
|
|
.padding(.horizontal, 20)
|
|
|
|
// Send Message Button
|
|
Button {
|
|
showingContactForm = true
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "paperplane.fill")
|
|
Text("Send Us a Message")
|
|
.fontWeight(.semibold)
|
|
}
|
|
.foregroundColor(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(16)
|
|
.background(Color(hex: getBrandColor()), in: RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
.padding(.horizontal, 20)
|
|
}
|
|
.padding(.bottom, 32)
|
|
|
|
// Support Our Ministry Section
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text("Support Our Ministry")
|
|
.font(.system(size: horizontalSizeClass == .regular ? 28 : 24, weight: .bold))
|
|
.padding(.horizontal, 20)
|
|
|
|
Text("Your generous gifts help us share the Three Angels' Messages and serve our community.")
|
|
.font(.system(size: horizontalSizeClass == .regular ? 18 : 16, weight: .regular))
|
|
.lineSpacing(4)
|
|
.padding(.horizontal, 20)
|
|
.foregroundColor(.secondary)
|
|
|
|
Link(destination: URL(string: getDonationUrl())!) {
|
|
HStack {
|
|
Image(systemName: "heart.fill")
|
|
Text("Give Securely Online")
|
|
.fontWeight(.semibold)
|
|
}
|
|
.foregroundColor(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(16)
|
|
.background(.green, in: RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
.padding(.horizontal, 20)
|
|
|
|
HStack {
|
|
Image(systemName: "lock.shield.fill")
|
|
.foregroundColor(.green)
|
|
Text("Secure giving powered by Adventist Giving")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.padding(.horizontal, 20)
|
|
}
|
|
.padding(.bottom, 100)
|
|
}
|
|
}
|
|
.navigationTitle("About Us")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.onAppear {
|
|
loadChurchConfig()
|
|
}
|
|
.sheet(isPresented: $showingContactForm) {
|
|
ContactFormView(isModal: true)
|
|
.environment(dataService)
|
|
}
|
|
}
|
|
|
|
private func loadChurchConfig() {
|
|
// Use Rust functions directly - NO JSON parsing in Swift!
|
|
self.churchConfig = ChurchConfig(
|
|
contactPhone: getContactPhone(),
|
|
contactEmail: getContactEmail(),
|
|
churchAddress: getChurchAddress(),
|
|
churchName: getChurchName()
|
|
)
|
|
}
|
|
}
|
|
|
|
struct ChurchMapView: View {
|
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
|
|
// Church location coordinates from Rust
|
|
private var churchLocation: CLLocationCoordinate2D {
|
|
let coords = getCoordinates()
|
|
return CLLocationCoordinate2D(
|
|
latitude: coords[0],
|
|
longitude: coords[1]
|
|
)
|
|
}
|
|
|
|
@State private var cameraPosition: MapCameraPosition = .region(
|
|
MKCoordinateRegion(
|
|
center: CLLocationCoordinate2D(latitude: 41.8703594, longitude: -72.4077036),
|
|
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
|
|
)
|
|
)
|
|
|
|
var body: some View {
|
|
Map(position: $cameraPosition) {
|
|
Annotation("Rockville Tolland SDA Church", coordinate: churchLocation) {
|
|
VStack {
|
|
Image(systemName: "house.fill")
|
|
.font(.title2)
|
|
.foregroundColor(.white)
|
|
.padding(8)
|
|
.background(Color(hex: getBrandColor()), in: Circle())
|
|
.shadow(radius: 4)
|
|
|
|
Text("RTSDA")
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(.white)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(Color.black.opacity(0.8), in: Capsule())
|
|
.shadow(radius: 3)
|
|
}
|
|
}
|
|
}
|
|
.frame(height: horizontalSizeClass == .regular ? 250 : 200)
|
|
.cornerRadius(16)
|
|
.onTapGesture {
|
|
// Open in Apple Maps
|
|
let placemark = MKPlacemark(coordinate: churchLocation)
|
|
let mapItem = MKMapItem(placemark: placemark)
|
|
mapItem.name = "Rockville Tolland SDA Church"
|
|
mapItem.openInMaps(launchOptions: [
|
|
MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving
|
|
])
|
|
}
|
|
.onAppear {
|
|
// Update camera position when config loads
|
|
cameraPosition = .region(
|
|
MKCoordinateRegion(
|
|
center: churchLocation,
|
|
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AngelMessageCard: View {
|
|
let number: Int
|
|
let title: String
|
|
let reference: String
|
|
let description: String
|
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
|
|
var body: some View {
|
|
VStack(spacing: 12) {
|
|
// Number circle
|
|
Text("\(number)")
|
|
.font(.system(size: horizontalSizeClass == .regular ? 28 : 24, weight: .bold))
|
|
.foregroundColor(.white)
|
|
.frame(width: horizontalSizeClass == .regular ? 50 : 44, height: horizontalSizeClass == .regular ? 50 : 44)
|
|
.background(Color(hex: getBrandColor()), in: Circle())
|
|
|
|
VStack(spacing: 8) {
|
|
Text(title)
|
|
.font(.system(size: horizontalSizeClass == .regular ? 18 : 16, weight: .semibold))
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text(reference)
|
|
.font(.system(size: horizontalSizeClass == .regular ? 14 : 12, weight: .medium))
|
|
.foregroundColor(Color(hex: getBrandColor()))
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(Color(hex: getBrandColor()).opacity(0.1), in: Capsule())
|
|
|
|
Text(description)
|
|
.font(.system(size: horizontalSizeClass == .regular ? 15 : 13))
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.lineLimit(4)
|
|
}
|
|
}
|
|
.padding(horizontalSizeClass == .regular ? 20 : 16)
|
|
.frame(maxWidth: .infinity)
|
|
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
|
|
}
|
|
}
|
|
|
|
struct ThreeAngelsMessagesView: View {
|
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
@State private var messages: [(title: String, reference: String, description: String)] = [
|
|
("First Angel's Message", "Revelation 14:6-7", "Fear God and give glory to Him, for the hour of His judgment has come"),
|
|
("Second Angel's Message", "Revelation 14:8", "Babylon is fallen, is fallen, that great city"),
|
|
("Third Angel's Message", "Revelation 14:9-12", "Keep the commandments of God and the faith of Jesus")
|
|
]
|
|
@State private var isLoading = true
|
|
|
|
private let angelMessages = [
|
|
(title: "First Angel's Message", reference: "Revelation 14:6-7"),
|
|
(title: "Second Angel's Message", reference: "Revelation 14:8"),
|
|
(title: "Third Angel's Message", reference: "Revelation 14:9-12")
|
|
]
|
|
|
|
var body: some View {
|
|
ForEach(Array(messages.enumerated()), id: \.offset) { index, message in
|
|
NavigationLink {
|
|
AngelMessageDetailView(
|
|
number: index + 1,
|
|
title: message.title,
|
|
reference: message.reference
|
|
)
|
|
} label: {
|
|
AngelMessageCard(
|
|
number: index + 1,
|
|
title: message.title,
|
|
reference: message.reference,
|
|
description: message.description
|
|
)
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
}
|
|
.task {
|
|
await loadMessages()
|
|
}
|
|
}
|
|
|
|
private func loadMessages() async {
|
|
var loadedMessages: [(String, String, String)] = []
|
|
|
|
for angel in angelMessages {
|
|
// Use church-core to fetch the actual Bible verse text (following BeliefsView pattern)
|
|
let versesJson = fetchBibleVerseJson(query: angel.reference)
|
|
|
|
// Use Rust function for JSON parsing and description generation - NO business logic in Swift!
|
|
let description = generateVerseDescription(versesJson: versesJson)
|
|
|
|
loadedMessages.append((angel.title, angel.reference, description))
|
|
}
|
|
|
|
await MainActor.run {
|
|
messages = loadedMessages
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AngelMessageDetailView: View {
|
|
let number: Int
|
|
let title: String
|
|
let reference: String
|
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
@State private var verseText: String = ""
|
|
@State private var isLoading: Bool = true
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(spacing: 24) {
|
|
// Header with number circle
|
|
VStack(spacing: 16) {
|
|
Text("\(number)")
|
|
.font(.system(size: horizontalSizeClass == .regular ? 48 : 40, weight: .bold))
|
|
.foregroundColor(.white)
|
|
.frame(width: horizontalSizeClass == .regular ? 80 : 70, height: horizontalSizeClass == .regular ? 80 : 70)
|
|
.background(Color(hex: getBrandColor()), in: Circle())
|
|
|
|
Text(title)
|
|
.font(.system(size: horizontalSizeClass == .regular ? 32 : 28, weight: .bold, design: .serif))
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text(reference)
|
|
.font(.system(size: horizontalSizeClass == .regular ? 20 : 18, weight: .medium))
|
|
.foregroundColor(Color(hex: getBrandColor()))
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 6)
|
|
.background(Color(hex: getBrandColor()).opacity(0.1), in: Capsule())
|
|
}
|
|
|
|
// Verse content
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
if isLoading {
|
|
ProgressView("Loading verse...")
|
|
} else if !verseText.isEmpty {
|
|
Text(verseText)
|
|
.font(.system(size: horizontalSizeClass == .regular ? 20 : 18, weight: .regular))
|
|
.lineSpacing(6)
|
|
.padding(24)
|
|
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
|
|
} else {
|
|
Text("Unable to load verse text")
|
|
.font(.system(size: horizontalSizeClass == .regular ? 18 : 16))
|
|
.foregroundColor(.secondary)
|
|
.padding(24)
|
|
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 20)
|
|
.padding(.bottom, 100)
|
|
}
|
|
.navigationTitle("Angel's Message")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.task {
|
|
await loadVerse()
|
|
}
|
|
}
|
|
|
|
private func loadVerse() async {
|
|
let versesJson = fetchBibleVerseJson(query: reference)
|
|
|
|
await MainActor.run {
|
|
isLoading = false
|
|
|
|
// Use Rust function for JSON parsing and text extraction - NO business logic in Swift!
|
|
verseText = extractFullVerseText(versesJson: versesJson)
|
|
}
|
|
}
|
|
}
|
|
|