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

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)
}
}