RTSDA-iOS/Views/Components/FeedItemCard.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

289 lines
9.8 KiB
Swift

import SwiftUI
struct FeedItemCard: View {
let item: FeedItem
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
var body: some View {
Group {
switch item.type {
case .sermon(let sermon):
SermonCard(sermon: sermon, style: .feed)
case .event(let event):
EventFeedCard(event: event)
case .bulletin(let bulletin):
BulletinFeedCard(bulletin: bulletin)
case .verse(let verse):
VerseFeedCard(verse: verse)
}
}
.containerRelativeFrame(.horizontal) { width, _ in
horizontalSizeClass == .regular ? min(width * 0.9, 600) : width * 0.95
}
}
}
// MARK: - Event Feed Card
struct EventFeedCard: View {
let event: ChurchEvent
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
var body: some View {
NavigationLink {
EventDetailViewWrapper(event: event)
} label: {
HStack(spacing: 16) {
// Date indicator
VStack(spacing: 4) {
Text(event.dayOfMonth)
.font(.system(size: 18, weight: .bold))
.foregroundColor(Color(hex: "fb8b23"))
Text(event.monthAbbreviation)
.font(.system(size: 10, weight: .semibold))
.foregroundColor(.secondary)
.textCase(.uppercase)
}
.frame(width: 50)
.padding(.vertical, 8)
.background(Color(hex: "fb8b23").opacity(Double(0.1)), in: RoundedRectangle(cornerRadius: 8))
// Event content
VStack(alignment: .leading, spacing: 8) {
Text(event.title)
.font(.system(size: 16, weight: .semibold))
.lineLimit(2)
if !event.description.isEmpty {
Text(event.description.stripHtml())
.font(.system(size: 14))
.foregroundColor(.secondary)
.lineLimit(2)
}
HStack(spacing: 8) {
Image(systemName: "clock.fill")
.font(.caption)
.foregroundColor(Color(hex: "fb8b23"))
Text(event.timeString)
.font(.system(size: 13, weight: .medium))
.foregroundColor(.secondary)
if !event.location.isEmpty {
Image(systemName: "location.fill")
.font(.caption)
.foregroundColor(Color(hex: "fb8b23"))
Text(event.location)
.font(.system(size: 13, weight: .medium))
.foregroundColor(.secondary)
.lineLimit(1)
}
}
}
Spacer()
// Event image (if available)
if let imageUrl = event.thumbnail ?? event.image {
AsyncImage(url: URL(string: imageUrl)) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
RoundedRectangle(cornerRadius: 8)
.fill(.secondary.opacity(0.3))
}
.frame(width: 60, height: 60)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
.padding(16)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4)
}
.buttonStyle(.plain)
}
private func dayFromDate(_ dateString: String) -> String {
let formatter = ISO8601DateFormatter()
if let date = formatter.date(from: dateString) {
let dayFormatter = DateFormatter()
dayFormatter.dateFormat = "d"
return dayFormatter.string(from: date)
}
return "?"
}
private func monthFromDate(_ dateString: String) -> String {
let formatter = ISO8601DateFormatter()
if let date = formatter.date(from: dateString) {
let monthFormatter = DateFormatter()
monthFormatter.dateFormat = "MMM"
return monthFormatter.string(from: date).uppercased()
}
return "???"
}
}
// MARK: - Bulletin Feed Card
struct BulletinFeedCard: View {
let bulletin: ChurchBulletin
var body: some View {
NavigationLink {
BulletinDetailView(bulletin: bulletin)
} label: {
HStack(spacing: 16) {
// PDF icon
Image(systemName: "doc.text.fill")
.font(.system(size: 32))
.foregroundStyle(Color(hex: "fb8b23"))
.frame(width: 50)
// Bulletin content
VStack(alignment: .leading, spacing: 8) {
Text(bulletin.title)
.font(.headline)
.fontWeight(.semibold)
.lineLimit(2)
.multilineTextAlignment(.leading)
Text(bulletin.formattedDate)
.font(.subheadline)
.foregroundStyle(.secondary)
Label("Church Bulletin", systemImage: "newspaper.fill")
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.green.opacity(0.1), in: Capsule())
.foregroundStyle(.green)
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(16)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4)
}
.buttonStyle(.plain)
}
}
// MARK: - Bible Verse Feed Card
struct VerseFeedCard: View {
let verse: BibleVerse
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Image(systemName: "book.fill")
.foregroundStyle(Color(hex: "fb8b23"))
Text("Verse of the Day")
.font(.caption)
.fontWeight(.medium)
.foregroundStyle(.secondary)
Spacer()
}
Text("\"\(verse.text)\"")
.font(.body)
.fontWeight(.medium)
.italic()
.lineLimit(4)
.multilineTextAlignment(.leading)
HStack {
Text(verse.reference)
.font(.caption)
.fontWeight(.semibold)
.foregroundStyle(Color(hex: "fb8b23"))
if let version = verse.version {
Text(version)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Button {
// Share verse
} label: {
Image(systemName: "square.and.arrow.up")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.padding(16)
.background(
LinearGradient(
colors: [Color(hex: "fb8b23").opacity(0.1), Color(hex: "fb8b23").opacity(0.05)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
in: RoundedRectangle(cornerRadius: 16)
)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color(hex: "fb8b23").opacity(0.2), lineWidth: 1)
)
}
}
// MARK: - Color Extension
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}
#Preview {
VStack(spacing: 20) {
FeedItemCard(item: FeedItem(
type: .sermon(Sermon.sampleSermon()),
timestamp: Date()
))
FeedItemCard(item: FeedItem(
type: .event(ChurchEvent.sampleEvent()),
timestamp: Date()
))
}
.padding()
}