168 lines
6.0 KiB
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)
|
|
}
|