import SwiftUI #if os(iOS) struct ShareSheet: UIViewControllerRepresentable { let activityItems: [Any] func makeUIViewController(context: Context) -> UIActivityViewController { UIActivityViewController(activityItems: activityItems, applicationActivities: nil) } func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} } #endif // MARK: - Sermon Card (for Watch view) enum SermonCardStyle { case watch // For Watch tab (compact) case feed // For Home feed (prominent with large thumbnail) } struct SermonCard: View { let sermon: Sermon let style: SermonCardStyle @Environment(\.horizontalSizeClass) private var horizontalSizeClass @State private var showingScriptureSheet = false @State private var showingShareSheet = false @State private var scriptureText = "" init(sermon: Sermon, style: SermonCardStyle = .watch) { self.sermon = sermon self.style = style } var body: some View { Button { if let videoUrl = sermon.videoUrl { let optimalUrl = getOptimalStreamingUrl(mediaId: sermon.id) let urlToUse = !optimalUrl.isEmpty ? optimalUrl : videoUrl if let url = URL(string: urlToUse) { SharedVideoManager.shared.playVideo(url: url, title: sermon.title, artworkURL: sermon.thumbnail) } } } label: { switch style { case .watch: watchStyleCard case .feed: feedStyleCard } } .buttonStyle(.plain) .contextMenu { Button("View Scripture", systemImage: "book.fill") { Task { let verses = fetchScriptureVersesForSermonJson(sermonId: sermon.id) await MainActor.run { scriptureText = verses showingScriptureSheet = true } } } Button("Share Sermon", systemImage: "square.and.arrow.up") { showingShareSheet = true } if sermon.audioUrl != nil && sermon.videoUrl == nil { Button("Play Audio Only", systemImage: "speaker.wave.2") { // TODO: Implement audio-only playback } } } .sheet(isPresented: $showingScriptureSheet) { ScriptureSheet(scriptureText: $scriptureText) } #if os(iOS) .sheet(isPresented: $showingShareSheet) { ShareSheet(activityItems: createShareItems(for: sermon)) } #endif .shadow(color: .black.opacity(0.1), radius: style == .feed ? 8 : 6, x: 0, y: style == .feed ? 4 : 2) } // MARK: - Watch Style (Compact) private var watchStyleCard: some View { Group { if horizontalSizeClass == .regular { // iPad: Horizontal layout HStack(spacing: 16) { thumbnailView() .frame(width: 120, height: 90) watchContentView Spacer() playButton } .padding(16) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12)) } else { // FIXED: Proper layout with real components VStack(alignment: .leading, spacing: 16) { thumbnailView(durationAlignment: .bottomTrailing) .frame(height: 160) watchContentView .padding(.horizontal, 16) .padding(.bottom, 16) } .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12)) } } } // MARK: - Feed Style (Prominent) private var feedStyleCard: some View { VStack(alignment: .leading, spacing: 0) { thumbnailView(durationAlignment: .bottomTrailing) .frame(height: horizontalSizeClass == .regular ? 200 : 180) // Content section feedContentView .padding(16) } .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16)) } private func thumbnailView(durationAlignment: Alignment = .bottomTrailing) -> some View { // FIXED: Use .fill but force proper clipping with frame Rectangle() .fill(.clear) .overlay { CachedAsyncImage(url: URL(string: sermon.thumbnail ?? sermon.image ?? "")) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { Rectangle() .fill(.gray) .overlay { Text("Loading...") .foregroundColor(.white) } } } .clipped() // Force clipping BEFORE clipShape .clipShape(RoundedRectangle(cornerRadius: 8)) .overlay(alignment: durationAlignment) { if let duration = sermon.durationFormatted { Text(duration) .font(.caption2) .fontWeight(.medium) .padding(.horizontal, 6) .padding(.vertical, 3) .background(.black.opacity(0.7), in: Capsule()) .foregroundStyle(.white) .padding(4) } } } // MARK: - Content Views private var watchContentView: some View { VStack(alignment: .leading, spacing: 8) { Text(sermon.title.trimmingCharacters(in: .whitespacesAndNewlines)) .font(.headline) .fontWeight(.semibold) .lineLimit(2) .multilineTextAlignment(.leading) if !sermon.speaker.isEmpty { Text("by \(sermon.speaker.trimmingCharacters(in: .whitespacesAndNewlines))") .font(.subheadline) .foregroundStyle(.secondary) .lineLimit(1) } if !sermon.formattedDate.isEmpty && sermon.formattedDate != "Date unknown" { Text(sermon.formattedDate) .font(.caption) .foregroundStyle(.secondary) } } } private var feedContentView: some View { VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 4) { Text(sermon.title.trimmingCharacters(in: .whitespacesAndNewlines)) .font(.headline) .fontWeight(.semibold) .lineLimit(2) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) if !sermon.speaker.isEmpty { Text("by \(sermon.speaker.trimmingCharacters(in: .whitespacesAndNewlines))") .font(.subheadline) .foregroundStyle(.secondary) } } if !sermon.formattedDate.isEmpty && sermon.formattedDate != "Date unknown" { Text(sermon.formattedDate) .font(.caption) .foregroundStyle(.secondary) } } } private var playButton: some View { Button { // Play sermon } label: { Image(systemName: "play.circle.fill") .font(.system(size: 24)) .foregroundStyle(Color(hex: "fb8b23")) } } } // MARK: - Event Card (for Connect view) struct EventCard: View { let event: ChurchEvent @Environment(\.horizontalSizeClass) private var horizontalSizeClass var body: some View { NavigationLink { EventDetailViewWrapper(event: event) } label: { VStack(alignment: .leading, spacing: 0) { // Event image or gradient Group { if let imageUrl = event.thumbnail ?? event.image { CachedAsyncImage(url: URL(string: imageUrl)) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { eventGradient } } else { eventGradient } } .frame(height: horizontalSizeClass == .regular ? 160 : 140) .clipped() // Content VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 4) { Text(event.title) .font(.headline) .fontWeight(.semibold) .lineLimit(2) .multilineTextAlignment(.leading) Text(event.formattedDate) .font(.subheadline) .foregroundStyle(.secondary) } if !event.location.isEmpty { Label { Text(event.location) .font(.caption) .lineLimit(1) } icon: { Image(systemName: "location.fill") .font(.caption) } .foregroundStyle(.secondary) } if !event.category.isEmpty { Text(event.category) .font(.caption) .fontWeight(.medium) .padding(.horizontal, 8) .padding(.vertical, 4) .background(.blue.opacity(0.1), in: Capsule()) .foregroundStyle(.blue) } } .padding(16) } .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12)) .shadow(color: .black.opacity(0.1), radius: 6, x: 0, y: 2) } .buttonStyle(.plain) } private var eventGradient: some View { LinearGradient( colors: [.blue, .blue.opacity(0.7)], startPoint: .topLeading, endPoint: .bottomTrailing ) .overlay { Image(systemName: "calendar") .font(.system(size: 32)) .foregroundStyle(.white.opacity(0.7)) } } } // MARK: - Bulletin Card (for Discover view) struct BulletinCard: View { let bulletin: ChurchBulletin var body: some View { NavigationLink { BulletinDetailView(bulletin: bulletin) } label: { VStack(alignment: .leading, spacing: 0) { // Header with PDF icon HStack { Image(systemName: "doc.text.fill") .font(.system(size: 32)) .foregroundStyle(Color(hex: "fb8b23")) VStack(alignment: .leading, spacing: 4) { Text("Church Bulletin") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) Text(bulletin.formattedDate) .font(.caption) .foregroundStyle(.secondary) } Spacer() if bulletin.pdfPath != nil { Image(systemName: "arrow.down.circle.fill") .font(.title3) .foregroundStyle(.green) } } .padding(16) .background(.regularMaterial) Divider() // Content preview VStack(alignment: .leading, spacing: 12) { Text(bulletin.title) .font(.headline) .fontWeight(.semibold) .lineLimit(2) .multilineTextAlignment(.leading) VStack(alignment: .leading, spacing: 4) { if !bulletin.sabbathSchool.isEmpty { Text("Sabbath School: \(bulletin.sabbathSchool)") .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } if !bulletin.divineWorship.isEmpty { Text("Divine Worship: \(bulletin.divineWorship)") .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } } HStack { Label("Tap to view", systemImage: "eye.fill") .font(.caption) .foregroundStyle(.blue) Spacer() if bulletin.pdfPath != nil { Label("PDF Available", systemImage: "doc.fill") .font(.caption) .foregroundStyle(.green) } } } .padding(16) } .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12)) .shadow(color: .black.opacity(0.1), radius: 6, x: 0, y: 2) } .buttonStyle(.plain) } } // MARK: - Quick Action Card struct QuickActionCard: View { let title: String let icon: String let color: Color let action: () -> Void var body: some View { Button(action: action) { VStack(spacing: 12) { Image(systemName: icon) .font(.system(size: 24)) .foregroundStyle(color) Text(title) .font(.subheadline) .fontWeight(.medium) .foregroundStyle(.primary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) .padding(20) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12)) .shadow(color: .black.opacity(0.1), radius: 6, x: 0, y: 2) } .buttonStyle(.plain) } } #Preview { ScrollView { VStack(spacing: 20) { SermonCard(sermon: Sermon.sampleSermon()) EventCard(event: ChurchEvent( id: "2", title: "Community Potluck Dinner", description: "Join us for fellowship", startTime: "2025-01-15T18:00:00-05:00", endTime: "2025-01-15T20:00:00-05:00", formattedTime: "6:00 PM - 8:00 PM", formattedDate: "January 15, 2025", formattedDateTime: "January 15, 2025 at 6:00 PM", dayOfMonth: "15", monthAbbreviation: "JAN", timeString: "6:00 PM - 8:00 PM", isMultiDay: false, detailedTimeDisplay: "6:00 PM - 8:00 PM", location: "Fellowship Hall", locationUrl: nil, image: nil, thumbnail: nil, category: "Social", isFeatured: false, recurringType: nil, createdAt: "2025-01-10T09:00:00-05:00", updatedAt: "2025-01-10T09:00:00-05:00" )) BulletinCard(bulletin: ChurchBulletin( id: "3", title: "January 11, 2025", date: "Saturday, January 11, 2025", sabbathSchool: "The Book of Romans", divineWorship: "Walking in Faith", scriptureReading: "Romans 8:28-39", sunset: "5:47 PM", pdfPath: "https://example.com/bulletin.pdf", coverImage: nil, isActive: true )) } .padding() } }