428 lines
14 KiB
Swift
428 lines
14 KiB
Swift
import SwiftUI
|
|
|
|
struct GymDetailView: View {
|
|
let gymId: UUID
|
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
@EnvironmentObject var themeManager: ThemeManager
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var showingDeleteAlert = false
|
|
|
|
private var gym: Gym? {
|
|
dataManager.gym(withId: gymId)
|
|
}
|
|
|
|
private var problems: [Problem] {
|
|
dataManager.problems(forGym: gymId)
|
|
}
|
|
|
|
private var sessions: [ClimbSession] {
|
|
dataManager.sessions(forGym: gymId)
|
|
}
|
|
|
|
private var gymAttempts: [Attempt] {
|
|
let problemIds = Set(problems.map { $0.id })
|
|
return dataManager.attempts.filter { problemIds.contains($0.problemId) }
|
|
}
|
|
|
|
private var gymStats: GymStats {
|
|
calculateGymStats()
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
LazyVStack(spacing: 20) {
|
|
if let gym = gym {
|
|
GymHeaderCard(gym: gym)
|
|
|
|
GymStatsCard(stats: gymStats)
|
|
|
|
if !problems.isEmpty {
|
|
RecentProblemsSection(problems: problems.prefix(5))
|
|
}
|
|
|
|
if !sessions.isEmpty {
|
|
RecentSessionsSection(sessions: sessions.prefix(3))
|
|
}
|
|
|
|
if problems.isEmpty && sessions.isEmpty {
|
|
EmptyGymStateView()
|
|
}
|
|
} else {
|
|
Text("Gym not found")
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
.navigationTitle(gym?.name ?? "Gym Details")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
|
if gym != nil {
|
|
Menu {
|
|
Button {
|
|
// Navigate to edit view
|
|
} label: {
|
|
Label("Edit Gym", systemImage: "pencil")
|
|
}
|
|
|
|
Button(role: .destructive) {
|
|
showingDeleteAlert = true
|
|
} label: {
|
|
Label("Delete Gym", systemImage: "trash")
|
|
}
|
|
} label: {
|
|
Image(systemName: "ellipsis.circle")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.alert("Delete Gym", isPresented: $showingDeleteAlert) {
|
|
Button("Cancel", role: .cancel) {}
|
|
Button("Delete", role: .destructive) {
|
|
if let gym = gym {
|
|
dataManager.deleteGym(gym)
|
|
dismiss()
|
|
}
|
|
}
|
|
} message: {
|
|
Text(
|
|
"Are you sure you want to delete this gym? This will also delete all problems and sessions associated with this gym."
|
|
)
|
|
}
|
|
}
|
|
|
|
private func calculateGymStats() -> GymStats {
|
|
let uniqueProblemsClimbed = Set(gymAttempts.map { $0.problemId }).count
|
|
let totalSessions = sessions.count
|
|
let activeSessions = sessions.count { $0.status == .active }
|
|
|
|
return GymStats(
|
|
totalProblems: problems.count,
|
|
totalSessions: totalSessions,
|
|
totalAttempts: gymAttempts.count,
|
|
uniqueProblemsClimbed: uniqueProblemsClimbed,
|
|
activeSessions: activeSessions
|
|
)
|
|
}
|
|
}
|
|
|
|
struct GymHeaderCard: View {
|
|
let gym: Gym
|
|
@EnvironmentObject var themeManager: ThemeManager
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text(gym.name)
|
|
.font(.title)
|
|
.fontWeight(.bold)
|
|
|
|
if let location = gym.location, !location.isEmpty {
|
|
Text(location)
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
if let notes = gym.notes, !notes.isEmpty {
|
|
Text(notes)
|
|
.font(.body)
|
|
.padding(.top, 4)
|
|
}
|
|
}
|
|
|
|
// Supported Climb Types
|
|
if !gym.supportedClimbTypes.isEmpty {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Climb Types")
|
|
.font(.headline)
|
|
.fontWeight(.semibold)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 8) {
|
|
ForEach(gym.supportedClimbTypes, id: \.self) { climbType in
|
|
Text(climbType.displayName)
|
|
.font(.caption)
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 6)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(themeManager.accentColor.opacity(0.1))
|
|
)
|
|
.foregroundColor(themeManager.accentColor)
|
|
}
|
|
}
|
|
.padding(.horizontal, 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Difficulty Systems
|
|
if !gym.difficultySystems.isEmpty {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Difficulty Systems")
|
|
.font(.headline)
|
|
.fontWeight(.semibold)
|
|
|
|
Text(gym.difficultySystems.map { $0.displayName }.joined(separator: ", "))
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 16)
|
|
.fill(.regularMaterial)
|
|
)
|
|
}
|
|
}
|
|
|
|
struct GymStatsCard: View {
|
|
let stats: GymStats
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text("Statistics")
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
|
|
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 16) {
|
|
StatItem(label: "Problems", value: "\(stats.totalProblems)")
|
|
StatItem(label: "Sessions", value: "\(stats.totalSessions)")
|
|
StatItem(label: "Total Attempts", value: "\(stats.totalAttempts)")
|
|
StatItem(label: "Problems Climbed", value: "\(stats.uniqueProblemsClimbed)")
|
|
}
|
|
|
|
if stats.activeSessions > 0 {
|
|
HStack {
|
|
StatItem(label: "Active Sessions", value: "\(stats.activeSessions)")
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 16)
|
|
.fill(.regularMaterial)
|
|
)
|
|
}
|
|
}
|
|
|
|
struct RecentProblemsSection: View {
|
|
let problems: any Sequence<Problem>
|
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text(
|
|
"Problems (\(dataManager.problems(forGym: Array(problems).first?.gymId ?? UUID()).count))"
|
|
)
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
|
|
LazyVStack(spacing: 12) {
|
|
ForEach(Array(problems), id: \.id) { problem in
|
|
NavigationLink(destination: ProblemDetailView(problemId: problem.id)) {
|
|
ProblemRowCard(problem: problem)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
if dataManager.problems(forGym: Array(problems).first?.gymId ?? UUID()).count > 5 {
|
|
Text(
|
|
"... and \(dataManager.problems(forGym: Array(problems).first?.gymId ?? UUID()).count - 5) more problems"
|
|
)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 16)
|
|
.fill(.regularMaterial)
|
|
)
|
|
}
|
|
}
|
|
|
|
struct RecentSessionsSection: View {
|
|
let sessions: any Sequence<ClimbSession>
|
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text(
|
|
"Recent Sessions (\(dataManager.sessions(forGym: Array(sessions).first?.gymId ?? UUID()).count))"
|
|
)
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
|
|
LazyVStack(spacing: 12) {
|
|
ForEach(Array(sessions), id: \.id) { session in
|
|
NavigationLink(destination: SessionDetailView(sessionId: session.id)) {
|
|
SessionRowCard(session: session)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
if dataManager.sessions(forGym: Array(sessions).first?.gymId ?? UUID()).count > 3 {
|
|
Text(
|
|
"... and \(dataManager.sessions(forGym: Array(sessions).first?.gymId ?? UUID()).count - 3) more sessions"
|
|
)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 16)
|
|
.fill(.regularMaterial)
|
|
)
|
|
}
|
|
}
|
|
|
|
struct ProblemRowCard: View {
|
|
let problem: Problem
|
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
|
|
private var problemAttempts: [Attempt] {
|
|
dataManager.attempts(forProblem: problem.id)
|
|
}
|
|
|
|
private var isCompleted: Bool {
|
|
problemAttempts.contains { $0.result.isSuccessful }
|
|
}
|
|
|
|
var body: some View {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(problem.name ?? "Unnamed Problem")
|
|
.font(.headline)
|
|
.fontWeight(.semibold)
|
|
.foregroundColor(.primary)
|
|
|
|
Text(
|
|
"\(problem.difficulty.grade) • \(problem.climbType.displayName) • \(problemAttempts.count) attempts"
|
|
)
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
if isCompleted {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.foregroundColor(.green)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(Color(uiColor: .secondarySystemGroupedBackground))
|
|
.shadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1)
|
|
)
|
|
}
|
|
}
|
|
|
|
struct SessionRowCard: View {
|
|
let session: ClimbSession
|
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
|
|
|
private var sessionAttempts: [Attempt] {
|
|
dataManager.attempts(forSession: session.id)
|
|
}
|
|
|
|
var body: some View {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
HStack {
|
|
Text(session.status == .active ? "Active Session" : "Session")
|
|
.font(.headline)
|
|
.fontWeight(.semibold)
|
|
.foregroundColor(.primary)
|
|
|
|
if session.status == .active {
|
|
Text("ACTIVE")
|
|
.font(.caption)
|
|
.fontWeight(.medium)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.fill(.green.opacity(0.2))
|
|
)
|
|
.foregroundColor(.green)
|
|
}
|
|
}
|
|
|
|
Text("\(formatDate(session.date)) • \(sessionAttempts.count) attempts")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
if let duration = session.duration {
|
|
Text("\(duration)min")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(Color(uiColor: .secondarySystemGroupedBackground))
|
|
.shadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1)
|
|
)
|
|
}
|
|
|
|
private func formatDate(_ date: Date) -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .medium
|
|
return formatter.string(from: date)
|
|
}
|
|
}
|
|
|
|
struct EmptyGymStateView: View {
|
|
var body: some View {
|
|
VStack(spacing: 20) {
|
|
Image(systemName: "figure.climbing")
|
|
.font(.system(size: 60))
|
|
.foregroundColor(.secondary)
|
|
|
|
VStack(spacing: 8) {
|
|
Text("No activity yet")
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
|
|
Text("Start a session or add problems to see them here")
|
|
.font(.body)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
}
|
|
.padding(40)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 16)
|
|
.fill(.regularMaterial)
|
|
)
|
|
}
|
|
}
|
|
|
|
struct GymStats {
|
|
let totalProblems: Int
|
|
let totalSessions: Int
|
|
let totalAttempts: Int
|
|
let uniqueProblemsClimbed: Int
|
|
let activeSessions: Int
|
|
}
|
|
|
|
#Preview {
|
|
NavigationView {
|
|
GymDetailView(gymId: UUID())
|
|
.environmentObject(ClimbingDataManager.preview)
|
|
}
|
|
}
|