diff --git a/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate b/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate index 3ff4e3c..63ede32 100644 Binary files a/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate and b/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios/Ascently/Views/AddEdit/AddEditProblemView.swift b/ios/Ascently/Views/AddEdit/AddEditProblemView.swift index 8ce67d7..f4a432b 100644 --- a/ios/Ascently/Views/AddEdit/AddEditProblemView.swift +++ b/ios/Ascently/Views/AddEdit/AddEditProblemView.swift @@ -19,6 +19,7 @@ struct AddEditProblemView: View { @State private var tags = "" @State private var notes = "" @State private var isActive = true + @State private var dateSet = Date() @State private var imagePaths: [String] = [] @State private var imageData: [Data] = [] @State private var showingPhotoOptions = false @@ -26,6 +27,9 @@ struct AddEditProblemView: View { @State private var showingImagePicker = false @State private var imageSource: UIImagePickerController.SourceType = .photoLibrary @State private var isEditing = false + @State private var showingGradeError = false + @State private var isLoaded = false + @State private var originalImages: [(path: String, data: Data)] = [] private var existingProblem: Problem? { guard let problemId = problemId else { return nil } @@ -52,6 +56,10 @@ struct AddEditProblemView: View { Form { GymSelectionSection() BasicInfoSection() + PhotosSection() + ClimbTypeSection() + DifficultySection() + LocationDetailsSection() AdditionalInfoSection() } .navigationTitle(isEditing ? "Edit Problem" : "New Problem") @@ -65,15 +73,22 @@ struct AddEditProblemView: View { ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { - saveProblem() + if canSave { + saveProblem() + } else { + showingGradeError = true + } } - .disabled(!canSave) + .disabled(!canSave && !showingGradeError) } } } .onAppear { setupInitialClimbType() loadExistingProblem() + DispatchQueue.main.async { + isLoaded = true + } } .sheet(isPresented: $showingPhotoOptions) { PhotoOptionSheet( @@ -155,43 +170,158 @@ struct AddEditProblemView: View { Section("Problem Details") { TextField("Problem Name (Optional)", text: $name) - VStack(alignment: .leading, spacing: 8) { - Text("Description (Optional)") - .font(.headline) + TextField("Description (Optional)", text: $description, axis: .vertical) + .lineLimit(3...6) + } + } - TextEditor(text: $description) - .frame(minHeight: 80) - .padding(8) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(.quaternary) - ) + @ViewBuilder + private func PhotosSection() -> some View { + Section("Photos (Optional)") { + Button(action: { + showingPhotoOptions = true + }) { + HStack { + Image(systemName: "camera.fill") + .foregroundColor(themeManager.accentColor) + + VStack(alignment: .leading) { + Text("Add Photos") + .foregroundColor(themeManager.accentColor) + Text("\(imageData.count) of 5 photos added") + .font(.caption) + .foregroundColor(.secondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption) + .foregroundColor(.secondary) + } + } + + if !imageData.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(imageData.indices, id: \.self) { index in + if let uiImage = UIImage(data: imageData[index]) { + ZStack(alignment: .topTrailing) { + Image(uiImage: uiImage) + .resizable() + .scaledToFill() + .frame(width: 80, height: 80) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + Button(action: { + imageData.remove(at: index) + }) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.white) + .background(Circle().fill(Color.black.opacity(0.5))) + } + .padding(4) + } + } + } + } + .padding(.vertical, 4) + } } } } @ViewBuilder - private func AdditionalInfoSection() -> some View { - Section("Additional Information") { - VStack(alignment: .leading, spacing: 8) { - Text("Notes (Optional)") - .font(.headline) - - TextEditor(text: $notes) - .frame(minHeight: 80) - .padding(8) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(.quaternary) - ) + private func ClimbTypeSection() -> some View { + Section("Climb Type") { + ForEach(ClimbType.allCases, id: \.self) { type in + HStack { + Text(type.displayName) + Spacer() + if selectedClimbType == type { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(themeManager.accentColor) + } else { + Image(systemName: "circle") + .foregroundColor(.secondary) + } + } + .contentShape(Rectangle()) + .onTapGesture { + selectedClimbType = type + } } + } + } + + @ViewBuilder + private func DifficultySection() -> some View { + Section("Difficulty") { + Picker("Difficulty System", selection: $selectedDifficultySystem) { + ForEach(DifficultySystem.systemsForClimbType(selectedClimbType), id: \.self) { system in + Text(system.displayName).tag(system) + } + } + .onChange(of: selectedDifficultySystem) { _ in + if isLoaded { + difficultyGrade = "" + } + } + + if selectedDifficultySystem == .custom { + HStack { + Text("Grade") + Spacer() + TextField("Numbers only", text: $difficultyGrade) + .multilineTextAlignment(.trailing) + .keyboardType(.numberPad) + } + } else { + Picker("Grade", selection: $difficultyGrade) { + if difficultyGrade.isEmpty { + Text("Select Grade").tag("") + } + ForEach(selectedDifficultySystem.availableGrades, id: \.self) { grade in + Text(grade).tag(grade) + } + } + .pickerStyle(.menu) + } + + if showingGradeError && difficultyGrade.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + Text("Please select a grade to continue") + .font(.caption) + .foregroundColor(.red) + .italic() + } + } + } + + @ViewBuilder + private func LocationDetailsSection() -> some View { + Section("Location & Details") { + TextField("e.g., 'Cave area', 'Wall 3'", text: $location) + + DatePicker("Date Set", selection: $dateSet, displayedComponents: .date) + } + } + + @ViewBuilder + private func AdditionalInfoSection() -> some View { + Section("Tags (Optional)") { + TextField("e.g., crimpy, dynamic (comma-separated)", text: $tags) + } + + Section("Additional Information") { + TextField("Notes (Optional)", text: $notes, axis: .vertical) + .lineLimit(3...6) Toggle("Problem is currently active", isOn: $isActive) } } private var canSave: Bool { - selectedGym != nil && difficultyGrade.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false + selectedGym != nil && !difficultyGrade.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } private func setupInitialClimbType() { @@ -199,7 +329,6 @@ struct AddEditProblemView: View { selectedGym = dataManager.gym(withId: gymId) } - // Always ensure a gym is selected if available and none is currently selected if selectedGym == nil && !dataManager.gyms.isEmpty { selectedGym = dataManager.gyms.first } @@ -219,13 +348,17 @@ struct AddEditProblemView: View { tags = problem.tags.joined(separator: ", ") notes = problem.notes ?? "" isActive = problem.isActive + if let date = problem.dateSet { + dateSet = date + } imagePaths = problem.imagePaths - // Load image data for preview imageData = [] + originalImages = [] for imagePath in problem.imagePaths { if let data = ImageManager.shared.loadImageData(fromPath: imagePath) { imageData.append(data) + originalImages.append((path: imagePath, data: data)) } } } @@ -242,12 +375,25 @@ struct AddEditProblemView: View { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty } - let tempImagePaths = imagePaths.filter { !$0.isEmpty && !imagePaths.contains($0) } - for imagePath in tempImagePaths { - _ = ImageManager.shared.deleteImage(atPath: imagePath) + var finalPaths: [String] = [] + var preservedPaths: Set = [] + + for data in imageData { + if let existing = originalImages.first(where: { $0.data == data }) { + finalPaths.append(existing.path) + preservedPaths.insert(existing.path) + } else { + if let newPath = ImageManager.shared.saveImageData(data) { + finalPaths.append(newPath) + } + } } - let newImagePaths = imagePaths.filter { !$0.isEmpty } + for original in originalImages { + if !preservedPaths.contains(original.path) { + _ = ImageManager.shared.deleteImage(atPath: original.path) + } + } if isEditing, let problem = existingProblem { let updatedProblem = problem.updated( @@ -257,8 +403,9 @@ struct AddEditProblemView: View { difficulty: DifficultyGrade(system: selectedDifficultySystem, grade: difficultyGrade), tags: trimmedTags, location: trimmedLocation.isEmpty ? nil : trimmedLocation, - imagePaths: newImagePaths.isEmpty ? [] : newImagePaths, + imagePaths: finalPaths, isActive: isActive, + dateSet: dateSet, notes: trimmedNotes.isEmpty ? nil : trimmedNotes ) dataManager.updateProblem(updatedProblem) @@ -272,8 +419,8 @@ struct AddEditProblemView: View { difficulty: DifficultyGrade(system: selectedDifficultySystem, grade: difficultyGrade), tags: trimmedTags, location: trimmedLocation.isEmpty ? nil : trimmedLocation, - imagePaths: newImagePaths.isEmpty ? [] : newImagePaths, - dateSet: Date(), + imagePaths: finalPaths, + dateSet: dateSet, notes: trimmedNotes.isEmpty ? nil : trimmedNotes ) dataManager.addProblem(problem)