// // ProblemsView.swift // OpenClimb // // Created by OpenClimb on 2025-01-17. // import SwiftUI struct ProblemsView: View { @EnvironmentObject var dataManager: ClimbingDataManager @State private var showingAddProblem = false @State private var selectedClimbType: ClimbType? @State private var selectedGym: Gym? @State private var searchText = "" private var filteredProblems: [Problem] { var filtered = dataManager.problems // Apply search filter if !searchText.isEmpty { filtered = filtered.filter { problem in (problem.name?.localizedCaseInsensitiveContains(searchText) ?? false) || (problem.description?.localizedCaseInsensitiveContains(searchText) ?? false) || (problem.location?.localizedCaseInsensitiveContains(searchText) ?? false) || (problem.setter?.localizedCaseInsensitiveContains(searchText) ?? false) || problem.tags.contains { $0.localizedCaseInsensitiveContains(searchText) } } } // Apply climb type filter if let climbType = selectedClimbType { filtered = filtered.filter { $0.climbType == climbType } } // Apply gym filter if let gym = selectedGym { filtered = filtered.filter { $0.gymId == gym.id } } return filtered.sorted { $0.updatedAt > $1.updatedAt } } var body: some View { NavigationView { VStack(spacing: 0) { if !dataManager.problems.isEmpty { FilterSection() .padding() .background(.regularMaterial) } if filteredProblems.isEmpty { EmptyProblemsView( isEmpty: dataManager.problems.isEmpty, isFiltered: !dataManager.problems.isEmpty ) } else { ProblemsList(problems: filteredProblems) } } .navigationTitle("Problems") .searchable(text: $searchText, prompt: "Search problems...") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { if !dataManager.gyms.isEmpty { Button("Add") { showingAddProblem = true } } } } .sheet(isPresented: $showingAddProblem) { AddEditProblemView() } } } } struct FilterSection: View { @EnvironmentObject var dataManager: ClimbingDataManager @State private var selectedClimbType: ClimbType? @State private var selectedGym: Gym? var body: some View { VStack(spacing: 12) { // Climb Type Filter VStack(alignment: .leading, spacing: 8) { Text("Climb Type") .font(.subheadline) .fontWeight(.medium) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { FilterChip( title: "All Types", isSelected: selectedClimbType == nil ) { selectedClimbType = nil } ForEach(ClimbType.allCases, id: \.self) { climbType in FilterChip( title: climbType.displayName, isSelected: selectedClimbType == climbType ) { selectedClimbType = climbType } } } .padding(.horizontal, 1) } } // Gym Filter VStack(alignment: .leading, spacing: 8) { Text("Gym") .font(.subheadline) .fontWeight(.medium) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { FilterChip( title: "All Gyms", isSelected: selectedGym == nil ) { selectedGym = nil } ForEach(dataManager.gyms, id: \.id) { gym in FilterChip( title: gym.name, isSelected: selectedGym?.id == gym.id ) { selectedGym = gym } } } .padding(.horizontal, 1) } } // Results count if selectedClimbType != nil || selectedGym != nil { HStack { Text( "Showing \(filteredProblems.count) of \(dataManager.problems.count) problems" ) .font(.caption) .foregroundColor(.secondary) Spacer() } } } } private var filteredProblems: [Problem] { var filtered = dataManager.problems if let climbType = selectedClimbType { filtered = filtered.filter { $0.climbType == climbType } } if let gym = selectedGym { filtered = filtered.filter { $0.gymId == gym.id } } return filtered } } struct FilterChip: View { let title: String let isSelected: Bool let action: () -> Void var body: some View { Button(action: action) { Text(title) .font(.caption) .fontWeight(.medium) .padding(.horizontal, 12) .padding(.vertical, 6) .background( RoundedRectangle(cornerRadius: 16) .fill(isSelected ? .blue : .clear) .stroke(.blue, lineWidth: 1) ) .foregroundColor(isSelected ? .white : .blue) } .buttonStyle(.plain) } } struct ProblemsList: View { let problems: [Problem] @EnvironmentObject var dataManager: ClimbingDataManager var body: some View { List(problems) { problem in NavigationLink(destination: ProblemDetailView(problemId: problem.id)) { ProblemRow(problem: problem) } } .listStyle(.plain) } } struct ProblemRow: View { let problem: Problem @EnvironmentObject var dataManager: ClimbingDataManager private var gym: Gym? { dataManager.gym(withId: problem.gymId) } var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { VStack(alignment: .leading, spacing: 4) { Text(problem.name ?? "Unnamed Problem") .font(.headline) .fontWeight(.semibold) Text(gym?.name ?? "Unknown Gym") .font(.subheadline) .foregroundColor(.secondary) } Spacer() VStack(alignment: .trailing, spacing: 4) { Text(problem.difficulty.grade) .font(.title2) .fontWeight(.bold) .foregroundColor(.blue) Text(problem.climbType.displayName) .font(.caption) .foregroundColor(.secondary) } } if let location = problem.location { Text("Location: \(location)") .font(.caption) .foregroundColor(.secondary) } if !problem.tags.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 6) { ForEach(problem.tags.prefix(3), id: \.self) { tag in Text(tag) .font(.caption2) .padding(.horizontal, 6) .padding(.vertical, 2) .background( RoundedRectangle(cornerRadius: 4) .fill(.blue.opacity(0.1)) ) .foregroundColor(.blue) } } } } if !problem.imagePaths.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(problem.imagePaths.prefix(3), id: \.self) { imagePath in AsyncImage(url: URL(fileURLWithPath: imagePath)) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { RoundedRectangle(cornerRadius: 8) .fill(.gray.opacity(0.3)) } .frame(width: 60, height: 60) .clipped() .cornerRadius(8) } } } } if !problem.isActive { Text("Inactive") .font(.caption) .foregroundColor(.red) .fontWeight(.medium) } } .padding(.vertical, 4) } } struct EmptyProblemsView: View { let isEmpty: Bool let isFiltered: Bool @EnvironmentObject var dataManager: ClimbingDataManager @State private var showingAddProblem = false var body: some View { VStack(spacing: 20) { Spacer() Image(systemName: "star.fill") .font(.system(size: 60)) .foregroundColor(.secondary) VStack(spacing: 8) { Text(title) .font(.title2) .fontWeight(.bold) Text(subtitle) .font(.body) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal) } if isEmpty && !dataManager.gyms.isEmpty { Button("Add Problem") { showingAddProblem = true } .buttonStyle(.borderedProminent) .controlSize(.large) } Spacer() } .sheet(isPresented: $showingAddProblem) { AddEditProblemView() } } private var title: String { if isEmpty { return dataManager.gyms.isEmpty ? "No Gyms Available" : "No Problems Yet" } else { return "No Problems Match Filters" } } private var subtitle: String { if isEmpty { return dataManager.gyms.isEmpty ? "Add a gym first to start tracking problems and routes!" : "Start tracking your favorite problems and routes!" } else { return "Try adjusting your filters to see more problems." } } } #Preview { ProblemsView() .environmentObject(ClimbingDataManager.preview) }