221 lines
8.4 KiB
Swift
221 lines
8.4 KiB
Swift
import SwiftUI
|
|
import Bedrock
|
|
|
|
/// Sheet presented when a ritual's arc completes, prompting the user to renew or end.
|
|
struct ArcRenewalSheet: View {
|
|
@Bindable var store: RitualStore
|
|
let ritual: Ritual
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
@State private var durationDays: Double
|
|
@State private var showingEditSheet = false
|
|
|
|
init(store: RitualStore, ritual: Ritual) {
|
|
self.store = store
|
|
self.ritual = ritual
|
|
_durationDays = State(initialValue: Double(ritual.defaultDurationDays))
|
|
}
|
|
|
|
private var completedArc: RitualArc? {
|
|
ritual.latestArc
|
|
}
|
|
|
|
private var arcSummary: String {
|
|
guard let arc = completedArc else { return "" }
|
|
let totalHabits = arc.habits.count
|
|
let totalCheckIns = arc.habits.reduce(0) { $0 + $1.completedDayIDs.count }
|
|
let possibleCheckIns = totalHabits * arc.durationDays
|
|
let rate = possibleCheckIns > 0 ? Int(Double(totalCheckIns) / Double(possibleCheckIns) * 100) : 0
|
|
return String(localized: "\(rate)% completion over \(arc.durationDays) days")
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.xLarge) {
|
|
celebrationHeader
|
|
summaryCard
|
|
durationSection
|
|
actionButtons
|
|
}
|
|
.padding(Design.Spacing.large)
|
|
}
|
|
.background(AppSurface.primary)
|
|
.navigationTitle(String(localized: "Ritual Complete"))
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button(String(localized: "Later")) {
|
|
store.dismissRenewalPrompt()
|
|
dismiss()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showingEditSheet) {
|
|
RitualEditSheet(store: store, ritual: ritual)
|
|
}
|
|
}
|
|
|
|
private var celebrationHeader: some View {
|
|
VStack(spacing: Design.Spacing.medium) {
|
|
Image(systemName: "checkmark.seal.fill")
|
|
.font(.system(size: 60))
|
|
.foregroundStyle(AppStatus.success)
|
|
|
|
Text(ritual.title)
|
|
.font(.title2.bold())
|
|
.foregroundStyle(AppTextColors.primary)
|
|
|
|
if let arc = completedArc {
|
|
Text(String(localized: "Arc \(arc.arcNumber) Complete"))
|
|
.font(.subheadline)
|
|
.foregroundStyle(AppTextColors.secondary)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, Design.Spacing.large)
|
|
}
|
|
|
|
private var summaryCard: some View {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
|
Text(String(localized: "Your Journey"))
|
|
.font(.headline)
|
|
.foregroundStyle(AppTextColors.primary)
|
|
|
|
HStack {
|
|
Image(systemName: "chart.line.uptrend.xyaxis")
|
|
.foregroundStyle(AppAccent.primary)
|
|
Text(arcSummary)
|
|
.foregroundStyle(AppTextColors.secondary)
|
|
}
|
|
.font(.subheadline)
|
|
|
|
if let arc = completedArc {
|
|
let habitCount = arc.habits.count
|
|
HStack {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.foregroundStyle(AppAccent.primary)
|
|
Text(String(localized: "\(habitCount) habits tracked"))
|
|
.foregroundStyle(AppTextColors.secondary)
|
|
}
|
|
.font(.subheadline)
|
|
}
|
|
}
|
|
.padding(Design.Spacing.large)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(AppSurface.secondary)
|
|
.clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.large))
|
|
}
|
|
|
|
private var durationSection: some View {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
|
Text(String(localized: "Next Arc Duration"))
|
|
.font(.headline)
|
|
.foregroundStyle(AppTextColors.primary)
|
|
|
|
VStack(spacing: Design.Spacing.small) {
|
|
HStack {
|
|
Text(String(localized: "\(Int(durationDays)) days"))
|
|
.font(.title3.bold())
|
|
.foregroundStyle(AppAccent.primary)
|
|
Spacer()
|
|
}
|
|
|
|
Slider(value: $durationDays, in: 7...365, step: 1)
|
|
.tint(AppAccent.primary)
|
|
|
|
HStack {
|
|
Text(String(localized: "1 week"))
|
|
.font(.caption)
|
|
.foregroundStyle(AppTextColors.tertiary)
|
|
Spacer()
|
|
Text(String(localized: "1 year"))
|
|
.font(.caption)
|
|
.foregroundStyle(AppTextColors.tertiary)
|
|
}
|
|
}
|
|
|
|
// Quick presets
|
|
HStack(spacing: Design.Spacing.small) {
|
|
ForEach([14, 21, 28, 30, 60, 90], id: \.self) { days in
|
|
Button {
|
|
durationDays = Double(days)
|
|
} label: {
|
|
Text("\(days)")
|
|
.font(.caption.bold())
|
|
.foregroundStyle(Int(durationDays) == days ? AppTextColors.primary : AppTextColors.secondary)
|
|
.padding(.horizontal, Design.Spacing.small)
|
|
.padding(.vertical, Design.Spacing.xSmall)
|
|
.background(Int(durationDays) == days ? AppAccent.primary : AppSurface.tertiary)
|
|
.clipShape(Capsule())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(Design.Spacing.large)
|
|
.background(AppSurface.secondary)
|
|
.clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.large))
|
|
}
|
|
|
|
private var actionButtons: some View {
|
|
VStack(spacing: Design.Spacing.medium) {
|
|
// Continue with same habits
|
|
Button {
|
|
store.renewArc(for: ritual, durationDays: Int(durationDays), copyHabits: true)
|
|
store.dismissRenewalPrompt()
|
|
dismiss()
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "arrow.clockwise")
|
|
Text(String(localized: "Continue with Same Habits"))
|
|
}
|
|
.font(.headline)
|
|
.foregroundStyle(AppTextColors.primary)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(Design.Spacing.medium)
|
|
.background(AppAccent.primary)
|
|
.clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
|
}
|
|
|
|
// Continue with changes
|
|
Button {
|
|
store.renewArc(for: ritual, durationDays: Int(durationDays), copyHabits: true)
|
|
store.dismissRenewalPrompt()
|
|
showingEditSheet = true
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "pencil")
|
|
Text(String(localized: "Continue with Changes"))
|
|
}
|
|
.font(.headline)
|
|
.foregroundStyle(AppAccent.primary)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(Design.Spacing.medium)
|
|
.background(AppSurface.secondary)
|
|
.clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
|
|
.stroke(AppAccent.primary, lineWidth: 1)
|
|
)
|
|
}
|
|
|
|
// End ritual
|
|
Button {
|
|
store.endArc(for: ritual)
|
|
store.dismissRenewalPrompt()
|
|
dismiss()
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "checkmark.circle")
|
|
Text(String(localized: "End This Ritual"))
|
|
}
|
|
.font(.subheadline)
|
|
.foregroundStyle(AppTextColors.secondary)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(Design.Spacing.medium)
|
|
}
|
|
}
|
|
}
|
|
}
|