import SwiftUI // MARK: - Rust Integration Data Models struct ContactFormData: Codable { let name: String let email: String let phone: String let message: String let subject: String } struct ValidationResult: Codable { let isValid: Bool let errors: [String] enum CodingKeys: String, CodingKey { case isValid = "is_valid" case errors } } struct ChurchConfig: Codable { let contactPhone: String let contactEmail: String let churchAddress: String let churchName: String enum CodingKeys: String, CodingKey { case contactPhone = "contact_phone" case contactEmail = "contact_email" case churchAddress = "church_address" case churchName = "church_name" } } struct ContactFormView: View { let isModal: Bool @State private var name = "" @State private var email = "" @State private var phone = "" @State private var subject = "General Inquiry" @State private var message = "" @State private var isSubmitting = false @State private var showingSuccess = false @State private var showingError = false @State private var showingAlert = false @State private var errorMessage = "" @State private var hasAttemptedSubmit = false @State private var churchConfig: ChurchConfig? @FocusState private var focusedField: Bool private let subjectOptions = [ "General Inquiry", "Prayer Request", "Bible Study Interest", "Membership Information", "Pastoral Care", "Adventist Youth", "Other" ] @Environment(\.dismiss) private var dismiss @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(ChurchDataService.self) private var dataService init(isModal: Bool = false) { self.isModal = isModal } private var alertMessage: String { showingSuccess ? "Thank you for reaching out! We'll get back to you as soon as possible." : errorMessage } private var headerSection: some View { VStack(alignment: .leading, spacing: 8) { Text("We'd love to hear from you! Send us a message and we'll get back to you as soon as possible.") .font(.subheadline) .foregroundStyle(.secondary) .lineSpacing(4) } } private var loadingOverlay: some View { Color.black.opacity(0.3) .ignoresSafeArea() .overlay { ProgressView() .progressViewStyle(CircularProgressViewStyle()) .scaleEffect(1.5) .tint(.orange) } } var body: some View { NavigationStack { Form { Section { headerSection } Section("Your Information") { VStack(alignment: .leading, spacing: 4) { CustomTextField(title: "Name", text: $name, placeholder: "Enter your full name") .focused($focusedField) if let nameError = getFieldError(for: "name") { HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.red) .font(.caption) Text(nameError) .font(.caption) .foregroundColor(.red) } } } VStack(alignment: .leading, spacing: 4) { CustomTextField(title: "Email", text: $email, placeholder: "Enter your email address") .keyboardType(.emailAddress) .autocapitalization(.none) .focused($focusedField) if let emailError = getFieldError(for: "email") { HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.red) .font(.caption) Text(emailError) .font(.caption) .foregroundColor(.red) } } } VStack(alignment: .leading, spacing: 4) { PhoneTextField( title: "Phone (Optional)", text: $phone, placeholder: "(555) 123-4567", icon: "phone.fill" ) .focused($focusedField) if let phoneError = getFieldError(for: "phone") { HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.red) .font(.caption) Text(phoneError) .font(.caption) .foregroundColor(.red) } } } } Section("Subject") { Menu { ForEach(subjectOptions, id: \.self) { option in Button(option) { subject = option } } } label: { HStack { Text(subject) .foregroundColor(.primary) Spacer() Image(systemName: "chevron.down") .foregroundColor(.secondary) .font(.caption) } } } Section("Message") { VStack(alignment: .leading, spacing: 4) { TextEditor(text: $message) .frame(minHeight: 120) .focused($focusedField) if let messageError = getFieldError(for: "message") { HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.red) .font(.caption) Text(messageError) .font(.caption) .foregroundColor(.red) } } } } Section { customSubmitButton if hasAttemptedSubmit && !validationResult.isValid { VStack(alignment: .leading, spacing: 8) { ForEach(validationResult.errors, id: \.self) { error in HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.red) Text(error) .font(.caption) .foregroundColor(.red) Spacer() } } } .padding() .background(.red.opacity(0.1), in: RoundedRectangle(cornerRadius: 8)) } } } .navigationTitle(isModal ? "Contact Us" : "Get in Touch") .navigationBarTitleDisplayMode(isModal ? .inline : .large) .toolbar { if isModal { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } } } .disabled(isSubmitting) .overlay { if isSubmitting { loadingOverlay } } .alert("Success", isPresented: $showingSuccess) { Button("OK") { if isModal { dismiss() } else { clearForm() } } } message: { Text("Thank you for reaching out! We'll get back to you as soon as possible.") } .alert("Error", isPresented: $showingError) { Button("OK") { } } message: { Text(errorMessage) } .onAppear { loadChurchConfig() } .onDisappear { focusedField = false } } private var customSubmitButton: some View { Button { submitForm() } label: { HStack { if isSubmitting { ProgressView() .progressViewStyle(CircularProgressViewStyle()) .scaleEffect(0.8) .tint(.white) Text("Sending...") } else { Image(systemName: "paperplane.fill") Text("Send Message") } } .font(.headline) .fontWeight(.semibold) .frame(maxWidth: .infinity) .padding(.vertical, 16) .foregroundStyle(.white) .background( (isFormValid && !isSubmitting) ? Color(hex: getBrandColor()) : Color(.systemGray3), in: RoundedRectangle(cornerRadius: 12) ) .shadow(color: isFormValid && !isSubmitting ? Color(hex: getBrandColor()).opacity(0.3) : Color.clear, radius: 4, y: 2) } .disabled(!isFormValid || isSubmitting) } private var quickContactOptions: some View { VStack(spacing: 16) { Divider() .padding(.vertical, 12) Text("Other Ways to Reach Us") .font(.subheadline) .fontWeight(.medium) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 16) VStack(spacing: 12) { // Only show Call Us on iPhone - iPads can't make phone calls if UIDevice.current.userInterfaceIdiom == .phone { QuickContactRow( icon: "phone.fill", title: "Call Us", subtitle: churchConfig?.contactPhone ?? "(860) 875-0450", color: .green ) { let phoneNumber = churchConfig?.contactPhone ?? "(860) 875-0450" let digitsOnly = phoneNumber.filter { $0.isNumber } let phoneURL = URL(string: "tel://\(digitsOnly)") // iPhone - make the call if let url = phoneURL, UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } } } QuickContactRow( icon: "envelope.fill", title: "Email Us", subtitle: getContactEmail(), color: .blue ) { if let url = URL(string: "mailto:\(getContactEmail())") { UIApplication.shared.open(url) } } QuickContactRow( icon: "location.fill", title: "Visit Us", subtitle: "9 Hartford Turnpike, Tolland, CT", color: .red ) { if let url = URL(string: "https://maps.apple.com/?address=9+Hartford+Turnpike,+Tolland,+CT+06084") { UIApplication.shared.open(url) } } } .padding(.horizontal, 16) } .listRowInsets(EdgeInsets()) } private var isFormValid: Bool { return validationResult.isValid } private var validationResult: ValidationResult { let formData = ContactFormData( name: name.trimmingCharacters(in: .whitespacesAndNewlines), email: email.trimmingCharacters(in: .whitespacesAndNewlines), phone: phone.trimmingCharacters(in: .whitespacesAndNewlines), message: message.trimmingCharacters(in: .whitespacesAndNewlines), subject: subject.trimmingCharacters(in: .whitespacesAndNewlines) ) guard let jsonData = try? JSONEncoder().encode(formData), let jsonString = String(data: jsonData, encoding: .utf8) else { return ValidationResult(isValid: false, errors: ["System error"]) } let resultJson = validateContactFormJson(formJson: jsonString) guard let data = resultJson.data(using: .utf8), let result = try? JSONDecoder().decode(ValidationResult.self, from: data) else { return ValidationResult(isValid: false, errors: ["System error"]) } return result } private var validationErrors: [String] { return validationResult.errors } private func getFieldError(for field: String) -> String? { // Only show errors after user has attempted to submit or field has content guard hasAttemptedSubmit || hasFieldContent(field) else { return nil } let errors = validationResult.errors return errors.first { error in switch field { case "name": return error.lowercased().contains("name") case "email": return error.lowercased().contains("email") case "phone": return error.lowercased().contains("phone") case "message": return error.lowercased().contains("message") default: return false } } } private func hasFieldContent(_ field: String) -> Bool { switch field { case "name": return !name.isEmpty case "email": return !email.isEmpty case "phone": return !phone.isEmpty case "message": return !message.isEmpty default: return false } } private func submitForm() { hasAttemptedSubmit = true guard isFormValid else { return } isSubmitting = true Task { let success = await dataService.submitContact( name: name.trimmingCharacters(in: .whitespacesAndNewlines), email: email.trimmingCharacters(in: .whitespacesAndNewlines), subject: subject.trimmingCharacters(in: .whitespacesAndNewlines), message: message.trimmingCharacters(in: .whitespacesAndNewlines), phone: phone.trimmingCharacters(in: .whitespacesAndNewlines) ) await MainActor.run { isSubmitting = false if success { showingSuccess = true } else { errorMessage = "Failed to send message. Please try again or contact us directly." showingError = true } } } } private func clearForm() { name = "" email = "" phone = "" subject = "General Inquiry" message = "" hasAttemptedSubmit = false } private func loadChurchConfig() { Task { let configJson = fetchConfigJson() // Parse the API response structure guard let data = configJson.data(using: .utf8), let response = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let configData = response["data"] as? [String: Any], let configDataJson = try? JSONSerialization.data(withJSONObject: configData), let config = try? JSONDecoder().decode(ChurchConfig.self, from: configDataJson) else { return } await MainActor.run { self.churchConfig = config } } } } // MARK: - Phone Text Field with Formatting // MARK: - Custom Text Field struct CustomTextField: View { let title: String @Binding var text: String let placeholder: String var body: some View { VStack(alignment: .leading, spacing: 8) { Text(title) .font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) TextField(placeholder, text: $text) .textFieldStyle(.plain) .padding() .background(.background, in: RoundedRectangle(cornerRadius: 8)) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(.quaternary, lineWidth: 1) ) } } } struct PhoneTextField: View { let title: String @Binding var text: String let placeholder: String let icon: String var errorMessage: String? = nil var body: some View { VStack(alignment: .leading, spacing: 12) { HStack { Image(systemName: icon) .foregroundStyle(Color(hex: getBrandColor())) .frame(width: 20) Text(title) .font(.subheadline) .fontWeight(.medium) } .padding(.top, 16) .padding(.leading, 16) TextField(placeholder, text: $text) .keyboardType(.phonePad) .textContentType(.telephoneNumber) .padding(16) .background(Color(.systemGray6), in: RoundedRectangle(cornerRadius: 8)) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(errorMessage != nil ? Color.red : Color(.systemGray4), lineWidth: errorMessage != nil ? 1.0 : 0.5) ) .onChange(of: text) { _, newValue in text = formatPhoneNumber(newValue) } if let errorMessage = errorMessage { HStack { Image(systemName: "exclamationmark.circle.fill") .foregroundColor(.red) .font(.caption) Text(errorMessage) .font(.caption) .foregroundColor(.red) } .padding(.leading, 20) } } .listRowInsets(EdgeInsets()) } private func formatPhoneNumber(_ input: String) -> String { // Remove all non-numeric characters let digitsOnly = input.filter { $0.isNumber } // Don't format if too long if digitsOnly.count > 10 { return String(digitsOnly.prefix(10)) } // Format based on length switch digitsOnly.count { case 0...3: return digitsOnly case 4...6: let area = String(digitsOnly.prefix(3)) let next = String(digitsOnly.dropFirst(3)) return "(\(area)) \(next)" case 7...10: let area = String(digitsOnly.prefix(3)) let middle = String(digitsOnly.dropFirst(3).prefix(3)) let last = String(digitsOnly.dropFirst(6)) return "(\(area)) \(middle)-\(last)" default: return digitsOnly } } } // MARK: - Custom Subject Picker struct CustomSubjectPicker: View { let title: String @Binding var selection: String let options: [String] let icon: String var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { Image(systemName: icon) .foregroundStyle(Color(hex: getBrandColor())) .frame(width: 20) Text(title) .font(.subheadline) .fontWeight(.medium) } Menu { ForEach(options, id: \.self) { option in Button(option) { selection = option } } } label: { HStack { Text(selection) .foregroundStyle(.primary) Spacer() Image(systemName: "chevron.up.chevron.down") .font(.caption2) .foregroundStyle(.secondary) } .padding(12) .background(Color(.systemGray6), in: RoundedRectangle(cornerRadius: 8)) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Color(.systemGray4), lineWidth: 0.5) ) } } } } // MARK: - Quick Contact Row struct QuickContactRow: View { let icon: String let title: String let subtitle: String let color: Color let action: () -> Void var body: some View { Button(action: action) { HStack(spacing: 12) { Image(systemName: icon) .font(.title3) .foregroundStyle(color) .frame(width: 24) VStack(alignment: .leading, spacing: 2) { Text(title) .font(.subheadline) .fontWeight(.medium) .foregroundStyle(.primary) Text(subtitle) .font(.caption) .foregroundStyle(.secondary) } Spacer() Image(systemName: "arrow.up.right.square") .font(.caption) .foregroundStyle(.secondary) } .padding(.vertical, 12) .padding(.horizontal, 16) .contentShape(Rectangle()) } .buttonStyle(.plain) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8)) } } // MARK: - Service Times Section struct ServiceTimesSection: View { var body: some View { VStack(alignment: .leading, spacing: 16) { Text("Service Times") .font(.headline) .fontWeight(.semibold) VStack(spacing: 12) { ServiceTimeRow( day: "Saturday", time: "9:15 AM", service: "Sabbath School" ) ServiceTimeRow( day: "Saturday", time: "11:00 AM", service: "Worship Service" ) ServiceTimeRow( day: "Wednesday", time: "6:30 PM", service: "Prayer Meeting" ) } } } } #Preview("Contact Form") { NavigationStack { ContactFormView() .environment(ChurchDataService.shared) } } #Preview("Modal Contact Form") { NavigationStack { ContactFormView(isModal: true) .environment(ChurchDataService.shared) } }