Andromida/Andromida/App/Views/History/HistoryView.swift

168 lines
6.0 KiB
Swift

//
// HistoryView.swift
// Andromida
//
// A scrollable calendar history view showing daily habit completion.
//
import SwiftUI
import Bedrock
/// Main history view with scrollable months and ritual filtering.
/// Wrapper struct to make Date identifiable for sheet presentation
struct IdentifiableDate: Identifiable {
let id = UUID()
let date: Date
}
struct HistoryView: View {
@Bindable var store: RitualStore
@State private var selectedRitual: Ritual?
@State private var selectedDateItem: IdentifiableDate?
@State private var showingExpandedHistory = false
private let calendar = Calendar.current
/// Generate months based on expanded state
/// - Collapsed: Last month + current month (2 months)
/// - Expanded: Up to 12 months of history
/// Months are ordered oldest first, newest last (chronological order)
private var months: [Date] {
let today = Date()
let currentMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: today)) ?? today
// Determine how far back to go
let monthsBack = showingExpandedHistory ? 11 : 1 // 12 months or 2 months total
guard let startMonth = calendar.date(byAdding: .month, value: -monthsBack, to: currentMonth) else {
return [currentMonth]
}
// Build list of months in chronological order (oldest first)
var result: [Date] = []
var current = startMonth
while current <= currentMonth {
result.append(current)
current = calendar.date(byAdding: .month, value: 1, to: current) ?? current
}
return result
}
/// Check if there's more history available beyond what's shown
private var hasMoreHistory: Bool {
guard let earliestActivity = store.earliestActivityDate() else { return false }
let today = Date()
guard let twoMonthsAgo = calendar.date(byAdding: .month, value: -1, to: today) else { return false }
return earliestActivity < twoMonthsAgo
}
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: Design.Spacing.large) {
// Header with Show More/Less button
headerSection
// Ritual filter picker
ritualPicker
// Month calendars
ForEach(months, id: \.self) { month in
HistoryMonthView(
month: month,
selectedRitual: selectedRitual,
completionRate: { date, ritual in
store.completionRate(for: date, ritual: ritual)
},
onDayTapped: { date in
selectedDateItem = IdentifiableDate(date: date)
}
)
}
}
.padding(Design.Spacing.large)
}
.background(LinearGradient(
colors: [AppSurface.primary, AppSurface.secondary],
startPoint: .topLeading,
endPoint: .bottomTrailing
))
.sheet(item: $selectedDateItem) { item in
HistoryDayDetailSheet(
date: item.date,
completions: store.habitCompletions(for: item.date, ritual: selectedRitual),
store: store
)
}
}
private var headerSection: some View {
HStack(alignment: .top) {
SectionHeaderView(
title: String(localized: "History"),
subtitle: String(localized: "Your journey over time")
)
Spacer()
if hasMoreHistory || showingExpandedHistory {
Button {
withAnimation(.easeInOut(duration: Design.Animation.standard)) {
showingExpandedHistory.toggle()
}
} label: {
HStack(spacing: Design.Spacing.xSmall) {
Text(showingExpandedHistory ? String(localized: "Show less") : String(localized: "Show more"))
.font(.subheadline)
Image(systemName: showingExpandedHistory ? "chevron.up" : "chevron.down")
.font(.caption)
}
.foregroundStyle(AppAccent.primary)
}
.buttonStyle(.plain)
}
}
}
private var ritualPicker: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Design.Spacing.small) {
// "All" option
filterChip(
title: String(localized: "All"),
isSelected: selectedRitual == nil,
action: { selectedRitual = nil }
)
// Individual rituals
ForEach(store.rituals) { ritual in
filterChip(
title: ritual.title,
isSelected: selectedRitual?.id == ritual.id,
action: { selectedRitual = ritual }
)
}
}
}
}
private func filterChip(title: String, isSelected: Bool, action: @escaping () -> Void) -> some View {
Button(action: action) {
Text(title)
.font(.subheadline)
.fontWeight(isSelected ? .semibold : .regular)
.foregroundStyle(isSelected ? AppTextColors.inverse : AppTextColors.primary)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.background(isSelected ? AppAccent.primary : AppSurface.card)
.clipShape(.capsule)
}
.buttonStyle(.plain)
.accessibilityAddTraits(isSelected ? .isSelected : [])
}
}
#Preview {
HistoryView(store: RitualStore.preview)
}