Andromida/Andromida/App/Views/Rituals/Sheets/ArcRenewalSheet.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)
}
}
}
}