1.5.0 Initial run as iOS in a monorepo

This commit is contained in:
2025-09-12 22:35:14 -06:00
parent f106244e57
commit 7da1893748
127 changed files with 7062 additions and 1039 deletions

View File

@@ -0,0 +1,216 @@
//
// AddEditGymView.swift
// OpenClimb
//
// Created by OpenClimb on 2025-01-17.
//
import SwiftUI
struct AddEditGymView: View {
let gymId: UUID?
@EnvironmentObject var dataManager: ClimbingDataManager
@Environment(\.dismiss) private var dismiss
@State private var name = ""
@State private var location = ""
@State private var notes = ""
@State private var selectedClimbTypes = Set<ClimbType>()
@State private var selectedDifficultySystems = Set<DifficultySystem>()
@State private var customDifficultyGrades: [String] = []
@State private var isEditing = false
private var existingGym: Gym? {
guard let gymId = gymId else { return nil }
return dataManager.gym(withId: gymId)
}
private var availableDifficultySystems: [DifficultySystem] {
if selectedClimbTypes.isEmpty {
return []
} else {
return selectedClimbTypes.flatMap { climbType in
DifficultySystem.systemsForClimbType(climbType)
}.removingDuplicates()
}
}
init(gymId: UUID? = nil) {
self.gymId = gymId
}
var body: some View {
NavigationView {
Form {
BasicInfoSection()
ClimbTypesSection()
DifficultySystemsSection()
NotesSection()
}
.navigationTitle(isEditing ? "Edit Gym" : "Add Gym")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
saveGym()
}
.disabled(!canSave)
}
}
}
.onAppear {
loadExistingGym()
}
.onChange(of: selectedClimbTypes) { _ in
updateAvailableDifficultySystems()
}
}
@ViewBuilder
private func BasicInfoSection() -> some View {
Section("Basic Information") {
TextField("Gym Name", text: $name)
TextField("Location (Optional)", text: $location)
}
}
@ViewBuilder
private func ClimbTypesSection() -> some View {
Section("Supported Climb Types") {
ForEach(ClimbType.allCases, id: \.self) { climbType in
HStack {
Text(climbType.displayName)
Spacer()
if selectedClimbTypes.contains(climbType) {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.blue)
} else {
Image(systemName: "circle")
.foregroundColor(.gray)
}
}
.contentShape(Rectangle())
.onTapGesture {
if selectedClimbTypes.contains(climbType) {
selectedClimbTypes.remove(climbType)
} else {
selectedClimbTypes.insert(climbType)
}
}
}
}
}
@ViewBuilder
private func DifficultySystemsSection() -> some View {
Section("Difficulty Systems") {
if selectedClimbTypes.isEmpty {
Text("Select climb types first to see available difficulty systems")
.foregroundColor(.secondary)
.font(.caption)
} else {
ForEach(availableDifficultySystems, id: \.self) { system in
HStack {
Text(system.displayName)
Spacer()
if selectedDifficultySystems.contains(system) {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.blue)
} else {
Image(systemName: "circle")
.foregroundColor(.gray)
}
}
.contentShape(Rectangle())
.onTapGesture {
if selectedDifficultySystems.contains(system) {
selectedDifficultySystems.remove(system)
} else {
selectedDifficultySystems.insert(system)
}
}
}
}
}
}
@ViewBuilder
private func NotesSection() -> some View {
Section("Notes (Optional)") {
TextEditor(text: $notes)
.frame(minHeight: 100)
}
}
private var canSave: Bool {
!name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !selectedClimbTypes.isEmpty
&& !selectedDifficultySystems.isEmpty
}
private func loadExistingGym() {
if let gym = existingGym {
isEditing = true
name = gym.name
location = gym.location ?? ""
notes = gym.notes ?? ""
selectedClimbTypes = Set(gym.supportedClimbTypes)
selectedDifficultySystems = Set(gym.difficultySystems)
customDifficultyGrades = gym.customDifficultyGrades
}
}
private func updateAvailableDifficultySystems() {
// Remove selected systems that are no longer available
let availableSet = Set(availableDifficultySystems)
selectedDifficultySystems = selectedDifficultySystems.intersection(availableSet)
}
private func saveGym() {
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedLocation = location.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedNotes = notes.trimmingCharacters(in: .whitespacesAndNewlines)
if isEditing, let gym = existingGym {
let updatedGym = gym.updated(
name: trimmedName,
location: trimmedLocation.isEmpty ? nil : trimmedLocation,
supportedClimbTypes: Array(selectedClimbTypes),
difficultySystems: Array(selectedDifficultySystems),
customDifficultyGrades: customDifficultyGrades,
notes: trimmedNotes.isEmpty ? nil : trimmedNotes
)
dataManager.updateGym(updatedGym)
} else {
let newGym = Gym(
name: trimmedName,
location: trimmedLocation.isEmpty ? nil : trimmedLocation,
supportedClimbTypes: Array(selectedClimbTypes),
difficultySystems: Array(selectedDifficultySystems),
customDifficultyGrades: customDifficultyGrades,
notes: trimmedNotes.isEmpty ? nil : trimmedNotes
)
dataManager.addGym(newGym)
}
dismiss()
}
}
extension Array where Element: Hashable {
func removingDuplicates() -> [Element] {
var seen = Set<Element>()
return filter { seen.insert($0).inserted }
}
}
#Preview {
AddEditGymView()
.environmentObject(ClimbingDataManager.preview)
}