
- 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
293 lines
10 KiB
Swift
293 lines
10 KiB
Swift
import SwiftUI
|
|
|
|
struct EventsListView: View {
|
|
@Environment(ChurchDataService.self) private var dataService
|
|
@State private var searchText = ""
|
|
@State private var selectedCategory = "All"
|
|
@State private var showingFilters = false
|
|
|
|
private let categories = ["All", "Service", "Ministry", "Social", "Other"]
|
|
|
|
private var grayBackgroundColor: Color {
|
|
#if os(iOS)
|
|
Color(.systemGray6)
|
|
#else
|
|
Color.gray.opacity(0.2)
|
|
#endif
|
|
}
|
|
|
|
private var systemBackgroundColor: Color {
|
|
#if os(iOS)
|
|
Color(.systemBackground)
|
|
#else
|
|
Color.black
|
|
#endif
|
|
}
|
|
|
|
var filteredEvents: [ChurchEvent] {
|
|
var events = dataService.events
|
|
|
|
// Filter by search text
|
|
events = SearchUtils.searchEvents(events, searchText: searchText)
|
|
|
|
// Filter by category
|
|
if selectedCategory != "All" {
|
|
events = events.filter { $0.category.lowercased() == selectedCategory.lowercased() }
|
|
}
|
|
|
|
return events
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Search and Filter Bar
|
|
VStack(spacing: 12) {
|
|
// Search Bar
|
|
HStack {
|
|
Image(systemName: "magnifyingglass")
|
|
.foregroundColor(.secondary)
|
|
|
|
TextField("Search events...", text: $searchText)
|
|
.textFieldStyle(PlainTextFieldStyle())
|
|
|
|
if !searchText.isEmpty {
|
|
Button(action: { searchText = "" }) {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 10)
|
|
.background(grayBackgroundColor)
|
|
.cornerRadius(10)
|
|
|
|
// Category Filter
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 8) {
|
|
ForEach(categories, id: \.self) { category in
|
|
Button(action: {
|
|
selectedCategory = category
|
|
}) {
|
|
Text(category)
|
|
.font(.subheadline)
|
|
.fontWeight(.medium)
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 8)
|
|
.background(selectedCategory == category ? Color.blue : grayBackgroundColor)
|
|
.foregroundColor(selectedCategory == category ? .white : .primary)
|
|
.cornerRadius(20)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 20)
|
|
}
|
|
}
|
|
.padding(.horizontal, 20)
|
|
.padding(.top, 8)
|
|
.padding(.bottom, 16)
|
|
.background(systemBackgroundColor)
|
|
|
|
// Events List
|
|
if dataService.isLoading {
|
|
Spacer()
|
|
ProgressView("Loading events...")
|
|
Spacer()
|
|
} else if filteredEvents.isEmpty {
|
|
Spacer()
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "calendar.badge.exclamationmark")
|
|
.font(.system(size: 50))
|
|
.foregroundColor(.secondary)
|
|
|
|
Text("No events found")
|
|
.font(.headline)
|
|
.foregroundColor(.secondary)
|
|
|
|
if !searchText.isEmpty || selectedCategory != "All" {
|
|
Text("Try adjusting your search or filters")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
}
|
|
.padding()
|
|
Spacer()
|
|
} else {
|
|
ScrollView {
|
|
LazyVStack(spacing: 16) {
|
|
ForEach(filteredEvents) { event in
|
|
NavigationLink(destination: EventDetailViewWrapper(event: event)
|
|
.environment(dataService)) {
|
|
EventListCard(event: event)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal, 20)
|
|
.padding(.bottom, 100)
|
|
}
|
|
.refreshable {
|
|
await dataService.loadAllEvents()
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Events")
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.large)
|
|
#endif
|
|
.task {
|
|
await dataService.loadAllEvents()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Event List Card Component
|
|
|
|
struct EventListCard: View {
|
|
let event: ChurchEvent
|
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
|
|
private var systemBackgroundColor: Color {
|
|
#if os(iOS)
|
|
Color(.systemBackground)
|
|
#else
|
|
Color.black
|
|
#endif
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
// Hero Image or Gradient
|
|
Group {
|
|
if let imageUrl = event.image, !imageUrl.isEmpty {
|
|
CachedAsyncImage(url: URL(string: imageUrl)) { image in
|
|
image
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fill)
|
|
} placeholder: {
|
|
gradientBackground
|
|
}
|
|
} else {
|
|
gradientBackground
|
|
}
|
|
}
|
|
.frame(height: horizontalSizeClass == .regular ? 180 : 140)
|
|
.clipped()
|
|
.overlay(
|
|
// Category Badge
|
|
VStack {
|
|
HStack {
|
|
if !event.category.isEmpty {
|
|
Text(event.category.uppercased())
|
|
.font(.caption2)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(.white)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(.ultraThinMaterial)
|
|
.cornerRadius(6)
|
|
}
|
|
Spacer()
|
|
|
|
if event.isFeatured {
|
|
Image(systemName: "star.fill")
|
|
.foregroundColor(.yellow)
|
|
.font(.caption)
|
|
}
|
|
}
|
|
Spacer()
|
|
}
|
|
.padding(12)
|
|
)
|
|
|
|
// Event Details
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text(event.title)
|
|
.font(.headline)
|
|
.fontWeight(.semibold)
|
|
.lineLimit(2)
|
|
.multilineTextAlignment(.leading)
|
|
|
|
// Date and Time
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "calendar")
|
|
.foregroundColor(.blue)
|
|
.font(.caption)
|
|
|
|
if event.isMultiDay {
|
|
// Multi-day: show formatted time which contains "Aug 30 - Aug 31 at 6 PM"
|
|
Text(event.formattedTime)
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
} else {
|
|
// Single day: show date and time separately
|
|
Text(event.formattedDateRange)
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
|
|
Text("•")
|
|
.foregroundColor(.secondary)
|
|
.font(.caption)
|
|
|
|
Text(event.formattedTime)
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
// Location
|
|
if !event.location.isEmpty {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "location")
|
|
.foregroundColor(.blue)
|
|
.font(.caption)
|
|
|
|
Text(event.location)
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
|
|
// Recurring indicator
|
|
if let recurringType = event.recurringType, !recurringType.isEmpty {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "repeat")
|
|
.foregroundColor(.orange)
|
|
.font(.caption)
|
|
|
|
Text(recurringType.capitalized)
|
|
.font(.caption)
|
|
.foregroundColor(.orange)
|
|
.fontWeight(.medium)
|
|
}
|
|
}
|
|
}
|
|
.padding(16)
|
|
}
|
|
.background(systemBackgroundColor)
|
|
.cornerRadius(12)
|
|
.shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2)
|
|
.contentShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
|
|
private var gradientBackground: some View {
|
|
LinearGradient(
|
|
colors: [
|
|
Color.blue.opacity(0.7),
|
|
Color.purple.opacity(0.6)
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
EventsListView()
|
|
.environment(ChurchDataService.shared)
|
|
}
|
|
}
|