
- 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
289 lines
9.8 KiB
Swift
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()
|
|
} |